Description
This proposal is based on a working implementation at:
https://github.com/yortus/TypeScript/tree/granular-targeting
To try it out, clone it or install it with npm install yortus-typescript
Problem Scenario
The TypeScript compiler accepts a single target
option of either ES3
, ES5
or ES6
. However, most realistic target environments support a mixture or ES5 and ES6, and even ES7, often known in advance (e.g. when targeting Node.js, and/or using polyfills).
Using TypeScript with target environments with mixed ES5/5/7 support presents some challenges, many of which have been discussed in other issues. E.g.:
- (Support compile targets between ES5 and ES6 #4389) Support compile targets between ES5 and ES6
- (Normalize our lib files by compiler settings #4168) Normalize our lib files by compiler settings
- (New APIs added to lib.d.ts may break client codes. Allow duplicated members in interfaces? Make lib.d.ts overridable? #3215) New APIs added to lib.d.ts may break client codes. Allow duplicated members in interfaces? Make lib.d.ts overridable?
- (Using ES6 type default library when targetting ES5 output #3005) Using ES6 type default library when targetting ES5 output
- (for-of does not work with some DOM collections when target is ES6 #2695) for-of does not work with DOM collections when target is ES6
- (somewhat related: (Create DOM-Level specific dom-version.d.ts #2481) Create DOM-Level specific dom-version.d.ts)
In summary:
- The default lib either includes all ES6 types and properties, or none of them.
- Specifying
--noLib
and/or manually maintaininglib.b.ts
files brings other problems:- separate core typings are a burden to maintain.
- problems of missing symbols and clashing symbols.
- burden of manually tracking fixes and additions made in the default libs.
- Targeting ES5 as the 'lowest common denominator' means some language features known to be supported cannot be used (eg generators in Node.js).
- Targeting ES6 (e.g. to take advantage of Node.js' support for many ES6 features) leads to further complications:
CommonJS modules won't compile, even though that's the only module system Node supports.(fixed by Support modules when targeting ES6 and an ES6 ModuleKind #4811)- The compiler emits ES6 even for features that are known not to be supported, which would fail at runtime.
- Adding babel.js to the build pipeline adds complexity.
Workarounds
To achieve mixed ES5/ES6 core typings:
- specify
--target ES5
and selectively add ES6 typings in separately maintained files (eg from DefinitelyTyped). - specify
--target ES6
and be careful to avoid referencing unsupported ES6 features (the compiler won't issue any errors). - specify
--noLib
and manually maintain custom core typings in your own project.
To use ES6 features supported by the target platform
- specify
--target ES5
and (a) accept that things will be down-level emitted, and (b) don't use features with no down-level emit yet (ie generators). - specify
--target ES6
and (a)convert everything from CommonJS to ES6 modules(fixed by Support modules when targeting ES6 and an ES6 ModuleKind #4811), (b) add babel.js to the build pipeline, and (c) configure babel.js to do either pass-through or down-level emit on a feature-by-feature basis.
Proposed Solution
This proposal consists of two parts:
1. Support for conditional compilation using #if
and #endif
directives, so that a single default lib can offer fine-grained typings tailored to a mixed ES3/5/6/7 target environment.
The conditional compilation part is detailed in a separate proposal (#4691) with its own working implementation.
1. A mechanism allowing the default lib to offer fine-grained typings tailored to a mixed ES3/5/6/7 target environment.
This is really an internal compiler detail, so the mechanism is open to debate. It just has to match the granularity supported by the new compiler options below.
The working implementation uses #if...#endif
conditional compilation proposed in #4691. But this is overkill for this use case and seems unlikely to be considered.
Several other mechanisms have been discussed (summarized here).
2. Support for additional compiler options allowing the target environment to be described on a feature-by-feature basis.
Under this proposal, the target
option remains, but is now interpreted as the 'baseline' target, determining which features the target supports by default. For instance, ES6 symbols and generators are supported by default if target
is set to ES6
or higher.
The additional compiler options have the form targetHasXYZ
, where XYZ
designates a feature. These options are used to override the target for a particular language feature. They instruct the compiler that the target environment explicitly does or does not support a particular feature, regardless of what the target
option otherwise imples.
The working implementation currently supports the following additional compiler options (all boolean):
targetHasArrowFunctions
: specify whether the target supports ES6() => {...}
syntaxtargetHasBlockScoping
: specify whether the target supports ES6let
andconst
targetHasForOf
: specify whether the target supports ES6for..of
syntaxtargetHasGenerators
: specify whether the target supports ES6 generatorstargetHasIterables
: specify whether the target supports ES6 iterables and iteratorstargetHasModules
: specify whether the target supports ES6 modulestargetHasPromises
: specify whether the target supports ES6 promisestargetHasSymbols
: specify whether the target supports ES6 symbols
These options work both on the command line and in tsconfig.json
files.
Example tsconfig.json
Files and their Behaviour
A.
{
"target": "es6",
"targetHasModules": false,
"targetHasBlockScoping": false,
"module": "commonjs"
}
Emits ES6 JavaScript, except with CommonJS module syntax, and with let
/const
down-leveled to var
. This might match a Node.js environment.
B.
{
"target": "es5",
"targetHasSymbols": true
}
Emits ES5 JavaScript, except with Symbol references emitted as-is, and with full type support for well-known symbols from the default lib.
C.
{
"target": "es5",
"targetHasPromises": true
}
Emits ES5 JavaScript, except with full type support for ES6 promises from the default lib. This would work in an ES5 environment with a native or polyfilled Promise
object.
Backward Compatibility, Design Impact, Performance, etc
- There is no impact on existing TypeScript projects. The additional options and preprocessor directives only modify the compiler's behaviour if they are explicitly used.
- The preprocessor directives
#if
and#endif
add new language syntax. No existing language features are affected. - There is negligable impact on compiler performance.
- Only one default lib is needed (
lib.es6.d.ts
). It contains many conditionally compiled sections (ie with#if
and#endif
)
Remaining Work and Questions
- Support compiler options for more target features, e.g.:
- template strings
- classes
Map
/Set
/WeakMap
/WeakSet
- binary and octal literals
- destructuring
- default, rest, and optional parameters
- How granular could/should targets be? Feature support is naturally hierarchical. E.g. block scoping may be separated into (a)
let
, (b)const
and (c) block-level function declaration. This is true of most features and their realistic implementations (the Kangax ES6 compatibility table has a three-level hierarchy down the left side).