-
Notifications
You must be signed in to change notification settings - Fork 13.2k
Description
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.
- myprogram
- mylib
- myotherlib
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
It could be interesting to also allow this for non-definition files, to allow similar functionality for "Proper external modules" as defined in Resolving dependency conflicts when importing node package typings #4665Proper external modules are definition files, and I think this proposal should apply to them as well.- It could be the case that the "opt-in" mechanism is not the right way to do things, and instead it should be based on something like the module resolution mechanism (if using node-style resolution, this functionality kicks in automatically)
- Somewhat inadvertently, this would fix
__MyLibbeing available globally in Guidance on writing declaration file for multi-targeted libraries #4337