Currently TypeScript compiler accepts a single compiler option, target, which specify both version of library to be included during compilation and version of JavaScript for emitting code together.
The behaviour presents limitations in two parts of the pipeline: consuming the default library and granularly controlling how JavaScript will be emitted (e.g. what features to be down-level etc.).
The proposal will mainly focus on the first issue regarding using the default library.
The current behavior of target option doesn't allow users to have a fine-grain control on what library or feature of the library to be included, and user cannot independently control library's version from emit JavaScript version. As summarized in #4692 _Proposal: Granular Targeting_ and #4168 _Normalize our lib files by compiler setting_
As @yortus points out in the #4692 _Proposal: Granular Targeting_ , there are possible workarounds for above issues by maintaing customized version of the default library while target another version of emit JavaScript.
Such approach is cumbersome to maintain and consume any necessary fixes of the default library.
There are two parts to the proposed solution:
--lib. The flag will be used to allow users to granularly control the default library.target flag will remain. Its behaviors with --lib flag will be discussed below.--libThe flag's options (see below for the full list) allows users to specify what library to be included into the compilation.
The flag's options can be separated into following categories:
es3
es5
es6
node
dom
dom.iterable
webworker
scripthost
es6.array
es6.collection
es6.function
es6.generator
es6.iterable
es6.math
es6.number
es6.object
es6.promise
es6.proxy
es6.reflect
es6.regexp
es6.string
es6.symbol
es6.symbol.wellknown
es7.array.include
The --lib flag is an optional flag and multiple options can be used in combination (e.g --lib es6,dom.iterable) and each option will be mapped to associated library.
So in this example, files: lib.es6.d.ts and lib.dom.iterable.d.ts will be loaded into the compilation..
When --lib is specified, --target will be used solely to indicate version of emitting JavaScript.
In additional to above behavior, we still need to determine what to be included when --lib is not specified, should it remain the same, including version of library specifying by --target and all host libraries, or including every library. -> _Current implementation is to include version of the library using --target
The proposal will only affect when the compiler attempts to create compilation in Program.ts. Other parts of the pipeline will not be affected by the proposal.
The --lib flag will take an options in the form of string separated by comma. Space will not be allowed as a separator. This rule apply to both command line and tsconfig
Valid
tsc --target es5 --lib es6
tsc --target es5 --lib es5,es6.array
"compilerOptions": {
...
"lib": "es5,es6.array"
}
Invalid command-line
tsc --target es5--lib es5, es6.array // es6.array will be considered a file-name
tsc --target es5 --lib es5 es6.array // es6.array will be considered a file-name
"compilerOptions": {
...
"lib": "es5, es6.array"
}
Thanks @yuit, great to see progress on this.
I'm wondering about how this handles cases where there is crossover between features. For example, consider the following project setup:
Promise is polyfilled by the project as necessary so we can rely on that.interface PromiseConstructor {
...
all<T>(values: ArrayLike<T | PromiseLike<T>>): Promise<T[]>; // Do include this
all<T>(values: Iterable<T | PromiseLike<T>>): Promise<T[]>; // DO NOT include this
}
Symbol.species and Symbol.toStringTag are not widely supported so should not be used in source code.For the above project, I'd like to tell tsc: _"I want to include definitions for Promise in the build, but NOT definitions for well-known symbols or methods that take generic iterables"_. That way, I get type safety for Promises and compile-time failure (rather than runtime failure) if I try to use unsupported constructs like Promise.all(someIterable).
I believe situations like this really get to the point of granular targeting. I've used Promise as an example but there are similar crossover issues with RegExp, Date, typed arrays, Map, Set, and other builtins. We want early failure for referencing things we already know aren't going to work reliably at run time. The compiler can really help here by letting us catch these errors at transpile time, rather than runtime.
I may have missed something, but I don't see any handling of feature crossover in this proposal as it is currently stated. It looks to me like the following would be the best that can be done:
Promise in es6.d.ts is currently all-or-nothing, including definitions that use well-known symbols and generic iterables, even if we explicitly want to avoid them.symbols.d.ts with well-known symbol declarations placed there, and taken out of places like promise.d.ts It could use declaration merging to add to existing declarations, like interface PromiseConstructor { [Symbol.species]: Function; }.symbols but _not_ promises, then we'll still have a Promise and PromiseConstructor defined by symbols.d.ts.This is where a tsc-internal mechanism that does the equivalent of conditional compilation shines - it starts with _all_ known definitions and filters out anything not supported on a line-by-line basis, which covers cross-over between features perfectly at arbitrary levels of granuarity. See for example this es6.d.ts. I'm NOT suggesting conditional compilation, just a purely internal mechanism that does effectively the same thing to get the correct definitions, with no more and no less than what the user asked for via --lib.
@yortus Thanks for the feedback and pointing out some holes in the proposal.
How to get just the right type definitions?
For the above project, I'd like to tell tsc: "I want to include definitions for Promise in the build, but NOT definitions for well-known symbols or methods that take generic iterables".
Yes you are right that the current proposal when you pick feature such as promise.d.ts, it is all or nothing. You cannot just pick only sub-part of the promise.d.t.
So building upon your proposed solution, we can 1) separate symbol into its own file (probably same as iterator) 2) separate symbol in Promise or other es6 features in to their own files like es6.promiseWithSymbol.d.ts which will get included when you specified that you want es6.symbol. I am trying to not to overly granularly separate the files so we don't end up creating large amounts of tiny lib. though for this as you suggest here there may not be too bad. Also, to clarify, I think these smaller files like es6.promiseWithSymbol.d.ts should not be exposed as options to users.
@yuit sounds reasonable :)
I am trying to not to overly granularly separate the files so we don't end up creating large amounts of tiny lib
I agree there are definitely diminishing returns to supporting finer granularity. However, I do think the choice of "grains" should be informed by how the browsers/JS runtimes actually add feature support over time, which is what this issue is meant to address after all.
For example, the Symbol builtin is widely supported now, but _well-known symbols_ still have very little support. I can definitely make use of Symbol in my node projects today, but I must avoid pretty much all well-known symbols. And they have pretty different use-cases anyway.
So, while I would not suggest having a separate flag for each of the 2 dozen or so well-known symbols, I would suggest having a separate flag for ES6.Symbol and for ES6.WellKnownSymbols, based on their actual availability.
I think these smaller files like es6.promiseWithSymbol.d.ts should not be exposed as options to users
Agreed, the number and names of all these .d.ts files can just be an implementation detail known to tsc. It can pick the right set of files according to a naming convention based on the supplied --lib flags.
@mhegazy note that it would be nice for one of the new lib.es6* files to be the minimal one for
tsc --target es6 --noLib node_modules/typescript/lib/lib.d.ts app.ts
(from https://github.com/Microsoft/TypeScript/issues/5504)
@alexeagle what you mean by lib.es6.* to be minimal? Do you mean you just want es5 lib? ( like what you describe in the issue #5504)?
First preference would be to support --target es6 with only the es5 lib, with a fix for #5504
If that's not possible for some reason, _then_ I'd like to have the lib.d.ts that expresses just the types needed by the compiler to make it work, to make the workaround less painful. (I have Duplicate identifier in our internal build because of the types I had to specify in our custom std lib)
@alexeagle Thanks for the clarification. The new compiler flag will allow you to get your preference fix for #5504 :smile:
You will now be able to do --target es6 --lib es5 (and include any es6 per-feature library)
This should be fixed by https://github.com/Microsoft/TypeScript/pull/7715. --lib support should be available now in typescript@next and in the next TypeScript release.
Going through and adding to alm. I know it says "lib": "es5, es6.array". But wouldn't "lib": ["es5","es6.array"] be better in tsconfig.json? Is this how future array options (if there aren't any yet) be represented in tsconfig.json's compilerOptions?
Thanks! :rose:
More questions:
getDefaultLibFileName in language service host (perhaps we should start going with FileNames (notice s) for these apis. getDefaultLibFileName should have an s suffix.es6.array or es2015.array. Note: the file is called lib.es2015.array.ts Here is a sample of the function I needed to delegate to eventually:
export const getDefaultLibFilePaths = (options: ts.CompilerOptions): string[] => {
if (options.noLib) {
return [];
}
if (options.lib) {
/** Note: this might need to be more fancy at some point. E.g. user types `es6.array` but we need to get `es2015.array` */
return options.lib.map((fileName) => fileFromLibFolder(`lib.${fileName}.d.ts`));
}
return [fileFromLibFolder(ts.getDefaultLibFileName(options))];
}
With this I have it working in alm. Feedback appreciated :rose:

Is this how future array options (if there aren't any yet) be represented in tsconfig.json's compilerOptions
In fact rootDirs is an array from the discussion : https://github.com/Microsoft/TypeScript/issues/8245#issuecomment-213945296 Maybe lib should be too in tsconfig.json
Verified by @2426021684 (https://github.com/TypeStrong/atom-typescript/issues/918#issuecomment-214076354) it is an array, as it should be :rose:
Update our decision on to ship node.d.ts or not to ship, the outcome is that we will not ship the node.d.ts as our default lib for the following reasons:
Most helpful comment
Update our decision on to ship node.d.ts or not to ship, the outcome is that we will not ship the node.d.ts as our default lib for the following reasons: