Skip to content

Proposal: Localized typings for definition files #4668

@jbrantly

Description

@jbrantly

This is another proposal for the issue explained in #2839 and #4665.

Problem

Ambient external module declarations are global in nature. For example, if the following is contained in a definition file included in the program...

declare module "myutils" { }

...then myutils can be referenced using an import statement anywhere in the entire program.

In contrast, npm packages can generally only import modules that they have explicitly declared as a dependency or that an ancestor in the dependency graph has declared as a dependency.

This difference can cause issues when multiple copies of (possibly different versions of) definition files are pulled in for the same package. This proposal will use the following example (from #2839) as a reference.

Proposal

To solve this, I propose an opt-in way for a definition file to declare that all imports/references contained in the definition file are localized only to that definition file. This would essentially allow definition files to define packages without accidentally leaking types to the global namespace, very similar to how an npm package does not make its dependencies directly available to the dependent package.

Syntactic changes

There would need to be a way for a definition file to say that it is opting-in to this feature. For example:

/// <references localized="true" />

declare module "myLib" { }

I'm not personally tied to what this looks like. If this proposal takes hold, let the bike-shedding begin.

Semantic changes

Any ambient declarations (modules, namespaces, variables, etc) that are referenced by the opting-in definition file are scoped to that file only and not made available to the program at large. This includes anything pulled in either through a /// <reference /> tag or an import statement. Ambient declarations actually made in the definition file are, of course, made available to the requesting scope (whatever that may be).

// myutils.1.0.d.ts

declare namespace MyUtils {
  export interface SomeInterface { }
}

declare module "myutils" {
  export = MyUtils;
}

// myutils.2.0.d.ts

declare module "myutils" {
  export interface RenamedInterface { }
}

// mylib.d.ts

/// <references localized="true" />
/// <reference path="myutils.1.0.d.ts" />

declare module "mylib" {
  import myutils = require('myutils');

  // if using the proposal in #2839, myutils.SomeInterface would not be available
  // but works with this proposal
  export var foo: myutils.SomeInterface;
}

// myotherlib.d.ts

/// <references localized="true" />
/// <reference path="myutils.2.0.d.ts" />

declare module "myotherlib" {
  import myutils = require('myutils');
  export var foo: myutils.RenamedInterface;
}

// myprogram.ts

/// <reference path="mylib.d.ts" />
/// <reference path="myotherlib.d.ts" />

// this will error since neither declaration of the "myutils" module was
// made available globally
import myutils = require('myutils');

// this will also error since the MyUtils namespace was not made
// available globally
var foo: MyUtils.SomeInterface;

Any ambient declarations (modules, namespaces, variables, etc) from a "higher level" are allowed, but will be overridden by any ambient declarations referenced by the definition file. No interface/module/namespace merging will occur.

// mylib.d.ts

/// <references localized="true" />
/// <reference path="myutils.1.0.d.ts" />

declare module "mylib" {
  import myutils = require('myutils');
  export var foo: myutils.SomeInterface;

  // this errors since the 1.0 "myutils" module overwrites
  // any inherited module (is not merged)
  export var bar: myutils.RenamedInterface;
}

// myotherlib.d.ts

/// <references localized="true" />

declare module "myotherlib" {
  // this still works even though there is no <reference /> for
  // it, inherited from higher level
  import myutils = require('myutils');
  export var foo: myutils.RenamedInterface;
}

// myprogram.ts

/// <reference path="myutils.2.0.d.ts" />
/// <reference path="mylib.d.ts" />

import myutils = require('myutils');

// will error, the 2.0 "myutils" module isn't somehow merged with 1.0
export var foo: myutils.SomeInterface;

// works
export var bar: myutils.RenamedInterface;

Other

Metadata

Metadata

Assignees

Labels

@typesRelates to working with .d.ts files (declaration/definition files) from DefinitelyTypedIn DiscussionNot yet reached consensusSuggestionAn idea for TypeScript

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions