Closed yortus closed 8 years ago
First off, I think @rbuckton has done/is doing some somewhat related work to this in a branch (along with other refactoring). So ping @rbuckton for some input.
Next up: This is probably just bikeshedding, but would it be possible to consider target: "ES3/5/6"
to desugar to some collection of these flags, and then make each flag an option like
target: {
"asyncAwait": false,
"decorators": false,
"arrowFunctions": true,
"blockScoping": true,
"forOf": true,
"generators": true,
"iterables": true,
"modules": {"emit": "commonjs"}, //or any of our other options or "esm" for ECMAscript module
"promises": true,
"symbols": true,
"templateLiterals": true,
"destructuring": true,
"defaultParameters": true,
"namespaces": false
}
Where the true
can additionally be a configuration object for the feature (as "modules"
above, which could deprecate the top-level "module" flag), for example, if the runtime supports let
but not const
, it could be indicated there. (Otherwise, if having configurable features seems wrong, it could simply be an array of strings, like babel's whitelist
argument.)
I'd rather like see the old target
syntax get deprecated for a purely flags-based one. (Though I imagine the old style will still get used as a shortcut for certain bundles of features.)
Mostly because I feel like using the target: string
style alongside all these top-level flags is building a huge amount of conditional complexity within the compiler (you must have seen the many places checking for ScriptVersion.ES6
and the module
flag while implementing this), and would like to see the former go away in favor of just the more versatile feature flags scheme to help unify how that feature checking is done internally.
More seriously: I like the proposed end result of this, by and large, but I don't like that it was coupled to the preprocessor feature (for reasons stated in that issue). So I'll help look at alternatives - as far as the lib file issue goes, I think this comes with the how we're trying to represent the lib as dependent on configuration without allowing the standard lib to be configured to an acceptable degree. (We pretty much have two settings right now.)
An alternate solution would be allowing one to specify, internally with each feature, lib.d.ts
parts for each, and parts which are dependent on other features to be included. Just like how .es6.d.ts
is conditionally included by target right now - Meaning we'd specify lib.d.ts
component flag dependencies in code in the compiler, rather than in the .d.ts
file with directives.
On top of that, we have to think how this interacts with alternate stdlibs, like the webworker lib, which is actually a bit of a pain to use at present. (Since it's not targetable by the compiler, you include it like any other ref and then it excludes the stdlib.) If we could break down what we'd like to include in our target standard lib with compiler flags in the same way we could feature emit, then we would have all of that information in the project, and let it be granularly targeted as well (and be dependent on emit target flags if need be). For example we could add the compiler options:
"stdlib": {
"environment": "browser", //or "worker" or "node" or a path to a ".d.ts" file
"configuration": { //Set of options used to build/include the correct ".d.ts" files
"DOMLevel": 3,
"canvas": true,
"webGL": true
}
}
The interesting thing about the stdlib is that from the compiler's perspective, it doesn't necessarily need to correspond to any files (though it does for simplicity's sake at present). See using string[]
vs Array<string>
to see what I mean (one's compiler intrinsic and defers to the other if possible, the other is stdlib). We could build the (higher-level, not string) contents of this .d.ts
- having a stdlib factory rather than an actual stdlib file. (Though for go to definition it would need to be able to generate a file, ofc, just like VM
"files" in the chrome dev tools)
Beyond that, it may be acceptable to include some kind of feature-dependency pragma within a triple-slash comment in an entire .d.ts
, which sets some kind of conditional inclusion/potential error for the entire file... but I don't feel like that's necessarily the right direction to go with this solution.
I agree with @weswigham 's idea of having the feature flags be under their own hash instead of at the top-level, if only to not collide with other top-level properties.
One question - would newly introduced feature flags (say for ES2016 features as they get standardized) default to true or false?
@Arnavion Likely false
unless a feature doesn't break back-compat when true (meaning it uses new syntax which doesn't change the interpretation of older code), at which point it would be decided on a per-feature basis, I imagine. Everything presently the default for an empty tsconfig
would likely start as true
. The defaults would likely be driven by backwards compatibility of config files until TS wanted to take a large breaking change.
(meaning it uses new syntax which doesn't change the interpretation of older code)
false
is good, but to clarify - the back-compat consideration is not just whether existing TS code gets broken, but whether adding new TS code that uses newly available features is allowed by default or not.
Eg: Say TS 1.7 gets released with support for the proposed bind operator ::
. If the switch were to default to true, someone may start using it in their project and not understand why it's not being downlevel-emitted, until they spelunked through the source or release notes to find the magic feature flag they must set to false.
@Arnavion I think that's something we'd have to configure via flags - I mean, we error on async/await unless you pass the flag to compile it, same with decorators. ::
would be the same way - we only recognize it as valid if we've been told to compile it. (Now, weather it should be recognized and not downleveled would need to be a configuration option on the feature - we don't do that at all right now except for features that we don't have a downlevel emit for, like generators)
I like the concept of the emit, but the addition of #if and #endif kind of scares me a little.
Instead of using conditional compilation in a base .d.ts
file, there should just be extra .d.ts
files included with each feature level.
In the past we discussed pre-processor directives like #if
and the general consensus is that we'd like to avoid adding that to the language.
We have been considering future support for "design-time" decorators, which only affect the compiler (and would not be written to the output file):
@@conditional("ES6")
- The body of the function/method is elided unless the named parameter is supplied to the compiler. Other examples could include: @@conditional("DEBUG")
, etc.@@profile("dom")
- The decorated member's type information is only visible when the "dom" profile is selected. Other examples could include: @@profile("es6")
, @@profile("webworker")
, etc.@@obsolete("message")
- Use of the decorated member in non-ambient code reports an error at compile time.We haven't settled on a final design yet.
@weswigham @Arnavion I agree that targets would be better represented by (possibly nestable) sub-hashes. But looking at tsc's commandLineParser.ts
, it is clearly oriented toward a flat list of top-level options with simple string/boolean values, which are then used to parse both the command line and tsconfig.json
files. So rather than proposing to also re-engineer this mechanism, I took the pragmatic way of just adding top-level options. I think that also overhauling the compiler options mechanism in the same proposal might distract from the key concept of granular targeting. But it would certainly make a good proposal on its own. It does have its own key questions, such as how to specify hierarchical options on the command line, which at this point is equally as capable as tsconfig.json
.
@Arnavion regarding default values: under this proposal if a particular option is not explicitly given, the default value is neither true
nor false
; it is determined by the target
option, which is either ES3
, ES5
, or ES6
. So for example, if your project does not explicitly specify an option for targetHasPromises
, then the compiler will look at the target
option. If that is >= ES6, then it will be as if you had specified targetHasPromises: true
, otherwise, it would be as if you had specified targetHasPromises: false
.
This means there is no danger of implicitly changing options when you upgrade the compiler.
@weswigham this is also a good reason for keeping (ie not deprecating) the target
option. It provides a succinct baseline that imples the value for all the other targetHas...
options if they are not explicitly overridden.
As is already the case, if you don't specify a target
, the compiler picks ES3
for you. So a blank tsconfig.json
file would imply target: ES3
(that is the current compiler behaviour), and hence false
for all the other targetHas...
options.
Alternatively if you set target: ES6
and nothing else, you would get true
for all the ES6 targetHas...
options, and false
for any ES7+ targetHas...
options.
@weswigham @LPGhatguy there no need for this proposal to be wedded to the #if...#endif
proposal. The important thing is to get just the core typings needed for the target features specified.
Some solutions to this:
#if...#endif
directives as is currently proposed (probably overkill since we just need conditional inclusion just in the core lib files)lib.d.ts
with triple-slash pragmas for conditionally including parts of the file lib.d.ts
file but have the compiler internally piece together the definitions it needs somehow, as suggested by @weswigham (although I suspect the cleanest way to implement this would probably involve a physical file with conditional pragmas behind the scenes)@rbuckton:
@@profile("dom") - The decorated member's type information is only visible when the "dom" profile is selected. Other examples could include: @@profile("es6"), @@profile("webworker"), etc.
If I understand correctly, this would work at compile time and in ambient contexts like lib.d.ts
. Is it effectively a conditional compilation mechanism, or something else? Also could such a decorator be specified on a property within a type? That would be needed to fully modularize the core types.
Eg would something like this be possible?
@@profile("targetHasPromises")
interface PromiseConstructor {
prototype: Promise<any>;
new <T>(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void): Promise<T>;
all<T>(values: ArrayLike<T | PromiseLike<T>>): Promise<T[]>;
@@profile("targetHasIterables")
all<T>(values: Iterable<T | PromiseLike<T>>): Promise<T[]>;
race<T>(values: ArrayLike<T | PromiseLike<T>>): Promise<T>;
@@profile("targetHasIterables")
race<T>(values: Iterable<T | PromiseLike<T>>): Promise<T>;
reject(reason: any): Promise<void>;
reject<T>(reason: any): Promise<T>;
resolve<T>(value: T | PromiseLike<T>): Promise<T>;
resolve(): Promise<void>;
@@profile("targetHasSymbols")
[Symbol.species]: Function;
}
@yortus:
it is clearly oriented toward a flat list of top-level options with simple string/boolean values, which are then used to parse both the command line and tsconfig.json files. So rather than proposing to also re-engineer this mechanism, I took the pragmatic way of just adding top-level options.
It's better to get it right first than have to go back and re-engineer it later while still also needing to support some half-done bit to get another feature out the door. The interface to the user is very much an important part of this feature, and deserves to be gotten right in a maintainable way.
this is also a good reason for keeping (ie not deprecating) the target option. It provides a succinct baseline that imples the value for all the other targetHas... options if they are not explicitly overridden.
No no, I mean we say this is still valid, this way old configs still work
"target": "ES5"
but identical to this in the new style (in effect, "ES5" expands to a set of flags):
"target": {
"asyncAwait": false,
"decorators": false,
"arrowFunctions": true,
"blockScoping": true,
"forOf": true,
"generators": false,
"iterables": false,
"modules": {"emit": "commonjs"},
"promises": false,
"symbols": false,
"templateLiterals": true,
"destructuring": true,
"defaultParameters": true,
"namespaces": true
}
And the two cannot coexist in the same command line/config (they're the same key). This means that if you want newer features or more granular control, you must swap your config to the newer syntax. Simple enough.
Having many sets of baselines plus flags that modify them is part of the current command-line-option-configuration-explosion problem.
one big lib.d.ts with triple-slash pragmas for conditionally including parts of the file as @weswigham suggested (effectively a form of conditional compilation)
No no, I would never suggest having a conditional pragma control something for only part of a file. I mean for extra metadata per-file. I don't like it very much, but I mean something like so (for, say symbols):
symbols.lib.ts
:
///<feature provides="symbols" requires="computedProperties">
interface Symbol {
toString(): string;
valueOf(): symbol;
[Symbol.toStringTag]: string;
}
interface SymbolConstructor {
prototype: Symbol;
(description?: string|number): symbol;
for(key: string): symbol;
keyFor(sym: symbol): string;
// Well-known Symbols
hasInstance: symbol;
match: symbol;
replace: symbol;
search: symbol;
species: symbol;
split: symbol;
toPrimitive: symbol;
toStringTag: symbol;
unscopables: symbol;
}
declare var Symbol: SymbolConstructor;
symbols+promises.lib.ts
:
///<feature provides="symbols" requires="computedProperties promises">
interface Promise<T> {
[Symbol.toStringTag]: string;
}
interface PromiseConstructor {
[Symbol.species]: Promise<any>;
}
//...
symbols+iterables.lib.ts
:
///<feature provides="symbols" requires="computedProperties iterables">
interface SymbolConstructor {
isConcatSpreadable: symbol;
iterator: symbol;
}
//...
(As a trimmed down example. Interface merging makes it all pretty nice.)
When a feature is enabled, all stdlib files which provides
that feature are loaded where their requires
are met. It's just a simple way of codifying the breakdowns which would need to happen internally, and by no means would I cram all that logic into a single massive conditionally compiling lib. And I'm not even suggesting something like this be publicly exposed - this would simply be an acceptable way to build up a final environment from many tiny parts by feature flags internally to the typescript compiler and language service, while still actually having readable individual "lib files". All of the constraints could be captured in code, though, so the comments are not really required, nor do they need to become part of the language.
IMO, avoiding any more ///
directives would be a good idea. I always feel that hiding meaningful syntax in comments is never really the right solution. Personally. I usually try to make a point of avoiding /// ref
's where possible, since external modules and tsconfig.json
exist to specify file dependencies nowadays.
@weswigham I apologise for misunderstanding your suggestions. I think your examples make it clear now.
So how to avoid the combinatorial explosion of tiny lib files? Just for every combination of symbols, promises, and iterables features, there might be:
symbols+promises+iterables.lib.ts
symbols+promises.lib.ts
symbols+iterables.lib.ts
promises+iterables.lib.ts
symbols.lib.ts
promises.lib.ts
iterables.lib.ts
I'm trying to work out how many files would be needed altogether for every combination of features that has core lib types. It's obviously the inter-dependent types that require the most chopping up. Actually its probably not a huge number and definitely a viable approach.
Anyway this can really be made just an internal implementation detail of the compiler, so the exact method of assembling the right types might come down to maintainability of the compiler. I'm not convinced the ///feature
pragma would come out easier or cleaner, but it may I guess.
Point taken about getting the proposal right with regard to using sub-hashes in the tsconfig.js
. As I mentioned above that was my original intention until I looked at commandLineParser.ts
, although that shouldn't have been relevent to the proposal. Any ideas how such config might be input on the command line?
No no, I mean we say this is still valid, this way old configs still work
"target": "ES5" but identical to this in the new style (in effect, "ES5" expands to a set of flags):
"target": { "asyncAwait": false, "decorators": false, ...
That's effectively how the proposal works, except that they are not mutually exclusive. The target
option does effectively expand into a set of feature flags, and the targetHas...
options just optionally override those. It actually makes the checker.ts
and emitter.ts
code clearer IMO, because instead of having things like:
if (languageVersion >= ScriptTarget.ES6 && node.asteriskToken) {
write("*");
}
it looks like:
if (languageVersion.hasGenerators && node.asteriskToken) {
write("*");
}
What I don't like about making target
mutually exclusive with the feature-specific options, is that:
target
you achieve brevity but cannot achieve any feature granularitytarget
you probably have to specify all the feature-specific options explicitly to get a deterministic build (the defaults might not be clear), and as more options are added to new tsc versions, you may get unexpected results anyway.With the current proposal you can be both concise and deterministic:
{
"target": "es5",
"targetHasPromises": true
}
It's clear here what all the omitted targetHas
options will be - they will be true
if they are <= ES5 features, and false
if they are >= ES6 features. And that won't change when building with a different tsc version.
@yortus Potentially instead of targetHas
, it might be better to just have an extensions
hash.
Instead of
{
"target": "es5",
"targetHasPromises": true
}
It might be
{
"target": "es5",
"extensions": {
"promises": true
}
}
@LPGhatguy yes, or even:
{
"target": {
"baseline": "es5",
"promises": true,
"blockScoping": true
}
}
That would provide a path to further sub-dividing features on an optional basis, eg:
{
"target": {
"baseline": "es5",
"promises": true,
"blockScoping": {
"baseline": false,
"let": true
}
}
}
So target
can be "ES3"|"ES5"|"ES6"|{...}
, and feature targets can be true|false|{...}
So how to avoid the combinatorial explosion of tiny lib files? Just for every combination of symbols, promises, and iterables features:
You don't. If you were using a single file to hold all that information, you'd have that same combinatoric expansion of configurations hidden behind a single monolith file, misleading you about the complexity hidden within it. Small files can be understandable, self-contained, and generally easy to reason about (if I edit/delete this file, it impacts this feature) - especially when they have explicit, well-defined dependencies. Monolith files are difficult to reason over as a unit and, usually, there's no desire to reason over them as a unit - you normally only care about a small part of them at a given instant, anyway. This is true both in code and in definitions.
Some large projects cough like TS cough started with somewhat grokkable, simple file/responsibility boundaries - as a project grows those can become less true. (For example, checker.ts
is 15728 lines and contains roughly 300 unique references to identifiers created in other files. It's massive and coupled to a lot of other code. At sha 214df64e (just a single year ago!), checker.ts
was just 5797 lines (and some number fewer references). We've almost tripled the code in the checker since TS was open sourced on github, apparently.) You have to make a conscious effort as you grow a project to keep breaking parts out just to compartmentalize the responsibilities of each component.
/rant
Any ideas how such config might be input on the command line?
There's a number of accepted syntaxes for this circulating in the JS community (depends on what opts parser you've used or if you've rolled your own, really), but, IMO, the best is just accepting an actual stringified JSON object on the command line once you're beyond the top level. Other argument nesting schemes are pretty much just unfamiliar cmd DSLs anyway. Others with nested configuration, like tslint, just require a config file. When your command line is longer than a few characters (ie, tsc
), who was typing them all out by hand each time, anyway? Hopefully nobody.
Anyways, there's a another what-does-target-mean issue here, though. At present, ES3/5/6
target means "accept and emit everything which can be emitted for these target API levels". Additional flags like --experimentalDecorators
augment those with additional features (and are blocked when they can't be downcompiled to the chosen target). We need the ability to control this targeted API level on a per-feature basis, for example, if I wanted to downlevel classes and disable destructuring:
{
"target": {
"baseline": "es6",
"classes": "es5",
"destructuring": false
}
}
But, then, what does "es6" vs "es5" vs "es3" mean in the case of classes? From experience, ES6 means preserve TS class
keyword, ES5 means emit with property descriptors on a constructor function, and ES3 means do best effort with members on a function. If we could quantify what these emits were dependent on in the environment (class
keyword, property descriptors) and target those features too, then we can safely compile to least common denominator given the backing features we claim to support. This would imply that the list of possible features in "target" goes well beyond the obvious top-level ones which would directly enable/disable high-level features.
But, then, what does "es6" vs "es5" vs "es3" mean in the case of classes?
That's why I wouldn't be keen on an option like "classes": "es5"
. It's unclear. But "classes":"false"
specifically means that ES6 classes cannot be emitted as-is. How the down-level emit works may be subject to other true/false
feature flags, or if it depends on something where there is no flag, the baseline ES3/5/6 target. This keeps all the flags decoupled. It also allows new ones to be added as needed to fine-tune the compiler's targeting abilities, without affecting the meaning of tsconfig
s that don't use the new flags.
One advantage of conditionally compiling declarations in lib.d.ts
files (whether by directives, pragmas, decorators or whatever) is that picking the right declarations is a completely orthogonal concern to how the declarations should be broken up across files. All of the following would be equally possible:
lib.d.ts
filesymbols.d.ts
, symbols+iterables.lib.ts
, promises+iterables.lib.ts
etcPromise
, WeakMap
, Symbol
etc)In the latter two cases there is no need for special logic to work out which files to include. Just grab them all and let the conditional compilation mechanism pick the right parts at compile time.
Very similar to #3538. :wink: and some of the commentary there I suspect would apply here as well.
Personally, I think full static resolution in tsconfig.json
is limiting. It would be better to allow expression of a run-time conditional which can potentially be statically defined at build time. That way, you give the end developer full control.
@kitsonk runtime is a bit late to apply granular targets - the JS has already been emitted and the types have already been checked.
Depends on what type of constructs... Polyfilling functionality, like Promises/WeakMaps/Sets/etc. is perfectly acceptable at runtime and you can make code that works transparently, of which if the end developer should be able to choose. It is true that largely TypeScript has avoided filling any of this functionality, but providing a generic feature flag mechanism should consider both the build and runtime functionality, otherwise it will likely be DOA.
When you are targeting fundamental language constructs like arrow functions, rest and spread operators, sure, the code is already emitted.
@kitsonk if your project polyfills some ES6 builtins, say Promise
and Map
, then you know statically that your target supports ES6 promises and maps, so you can tell the compiler to statically type-check the code involving promises and maps. The compiler won't know whether they are native promises or bluebird promises, but that won't matter for type checking.
Since you can't postpone type checking to runtime, and TypeScript already has no problem with polyfilling, I'm not sure what kind of runtime checks you are suggesting would be better? Can you give a more specific example?
As an end user, sometimes I might want to create a build that is a bit more "bloated" but allows run-time code path selection, because maybe I am not 100% sure of my user base and I want to hedge my bets, or while I am in development, I don't want to create all my target builds for all my end user agents. But once everything is settled down, I want to create my final "distribution" I will then want to statically define my features and have a slimmed down targeted emit.
I am suggesting something like has.js in combination with allowing the flags to be statically defined at build time. That way, the end developer has a choice of controlling the emit. For example:
has.add('es6-promise', typeof Promise === 'function');
if (has('es6-promise')) {
console.log('I Promise to always love you');
}
else {
console.log('I cannot Promise you anything.');
}
When the feature es6-promise
is not statically defined, the code would get emitted as a whole, leaving the code path to be determined at run time. But if statically defined, tree shaking would occur. So if "es6-promise": true
:
console.log('I Promise to always love you');
And if "es6-promise": false
:
console.log('I cannot Promise you anything.');
Of course flags could be expressed with no run-time evaluation, ones where only the target and the compiler will change the emit.
@kitsonk I think I get you now, if I understand correctly you are proposing (a) using runtime constants that may be compile-time constants known by the compiler, and (b) doing some control flow analysis and dead code elimination.
@yortus yes, a compiler constant can be a run-time variable, or just a compiler constant and a run-time variable might be overriden by a compiler constant or just have it's run-time representation. When the compiler constant is used the AST is shaken for dead code removal.
@kitsonk got it. Surely compile-time constants and dead-code elimination would be useful for things other than just granular targeting of ES features (eg eliding debug calls in production builds, etc). If that's the case, wouldn't it be better handled as a feature on its own, with its own proposal? Both proposals are useful with or without the other, they just happen to have a sweet spot where they overlap (ie having both proposals implemented would allow the full control you spoke of).
+1 "Emits ES6 JavaScript, except with CommonJS module syntax, and with let/const down-leveled to var.."
Exactly what I need.
@yortus I am not trying to derail this proposal, but I guess what I am saying is that I don't think it is a full solution. What I am trying to say is that a full solution would address feature flags as a whole incorporating a run-time semantic as well which would be built out. For example Promises/Sets/WeakMaps/Maps/Harmony Object.observe... The TypeScript team have made it clear they don't want to (and I agree with them) provide a lot of higher order functionality constructs that is part of the ES standards (or the DOM for that matter). They are only interested in the language semantics. If the proposed solution doesn't solve how to deal with those type of features, or give the end developer a framework to express those features...
Essentially #3538 is that proposal that I have been talking about here, though I am more arguing the concept that build-time feature flags should complement some sort of run-time feature expression and code path choices.
I am more arguing the concept that build-time feature flags should complement some sort of run-time feature expression and code path choices.
@kitsonk I have no objection to that. The key word there for me is complement, as in they are complementary proposals that would work well together. You seem to be suggesting that they should all be put into one proposal. I guess I'm not convinced of that. On the static side, the issue is granular static type checking (by conditionally bringing in the right bits of the default lib) and granular choice over what is emitted as-is and what is down-leveled. On the dynamic side, there is control flow analysis and dead code elimination. That would be a lot to tackle in a single proposal, especially when they can be considered separately and still work perfectly fine together.
Runtime implementation and polyfill is the job of polyfills like core-js
, not TypeScript. That will provide automatic polyfilling now and going forward into the future.
TypeScript's language filling are all syntactical, not practical to detect at runtime, and would be nice to decide the level of.
One of the main draws of TS is supposed to be the support for newer syntax, but the current implementation doesn't allow for taking advantage of it without compromises or workarounds, and it significantly detracts from the appeal of TS. It makes sense that TS doesn't do runtime stuff, but it doesn't make sense that I have to target ES5 for platforms that support ES6 features, or that I have to use a tool like babel that duplicates syntax related functions of TS if I want to target ES6 because the targeting is all or nothing.
I just wanted to see if there's any renewal of this issue. I just discovered with https://github.com/TypeStrong/ts-node/issues/43#issuecomment-160785145 that node supports some ES6 features but doesn't seem to support others such as destructuring. Seems like a pretty major bottleneck for people wanting to use ES6 features but using a runtime like node. It would be great to have something like:
{
"target": "ES6",
"features": {
"destructuring": false
}
}
Where the compiler can support certain downgrades/upgrades in syntax output based on the initial target.
I'd also be interested if the team has had further thoughts on this. In the v1.7 release (congrats team on this and thanks!) blog post it states:
For example, it is now a breeze to target Node.js v4 and beyond, which doesn't support ES6 modules (but does support several other ES6 features).
//tsconfig.json targeting node.js v4 and beyond
{
"compilerOptions": {
"module": "commonjs",
"target": "es6"
}
}
But to take @blakeembrey's example, anybody doing this and using destructuring in their code will find themselves getting runtime errors from node but no errors from tsc
. And more of this kind of situation can be expected in future as ES7 matures.
For Node 5, I'm currently using a two-step build/run:
tsconfig.json
:
{
"compilerOptions": {
"target": "ES6"
}
}
Then a final pass through babel (requires npm i babel-preset-node5
, a little preset I wrote to polyfill most of the missing 41% of the ES2015 spec in Node 5.x)
.babelrc
{
"presets": [
"node5"
]
}
... which can now be run with babel-node [script.js]
Benefits:
babel-node
everywhere you'd run node
I've put together a 7 minute tutorial video that provides a more in-depth how-to:
Targeting node via typescript without babel is still a mess due to lack of granular targeting. I updated to TypeScript 1.7.3 and the spread operator fails on node yet compiles fine with tsc.
If toggles for each feature are going to take a while then maybe a node
target would be a good short term fix. It doesn't make sense to only have ES targets when in the real world support varies on a feature by feature basis. Ultimately I think babel has the correct approach (preset options plus individual feature toggles).
@bootstraponline I agree. Without granular (i.e. per feature) targets, native node targeting is going to be tough.
The problem is that Node ES2015 support differs by version. In the 4.x branch, it's 49%. In 5.x, it's 54%. If TypeScript allow a single "node" target, it would mean polyfilling the missing features in the lowest usable node version, which is probably still the 0.10+ branch. And then of course there's MS's Chakra-based Node fork, which is at 90%. The majority of Node users are still going to find native features "over-transpiled" where it's not necessary.
The neat solution is a preset/plugin option, like babel.
For now, specifically targeting Node 4/5 with my babel-preset-node5 preset and then targeting "es6" with tsc
should do the trick. It's an extra step, but it's generally a small one. Just configure your build dev to run babel-node
instead of plain node
and that fixes most things.
@leebenson @bootstraponline looks like an external solution will be the way to go for a good while yet. I can't speak for the language team but I have noticed that of the many features added to the roadmap recently, right out to v2.1, granular targeting is not one of them
yup. Add babel and be done with it (for now.) It adds ~1s to the build time but otherwise, is pretty much invisible.
@leebenson unfortunately that still doesn't help with the lib.d.ts
issues outlined in the OP, which can be just as painful.
@yortus understood. That hadn't been an issue I came across, but I can see how that could be a show-stopper.
My current build pipeline is emit ES6 with async/await, and then pipe in to Babel and down emit an __awaiter and use Generators, supported in Node 5.x.
The __awaiter is designed to allow drop-in Promise replacement but the lib.es6.d.ts tramples that by declaring a global Promise type. That's fair enough as ES6 has native promises.
I would like to not use native promises but instead use Bluebird and it's bluebird.d.ts.
Is there another issue that covers this one more accurately? Or a solution? Can I just pull Promise from the lib.es6.d.ts definition?
Edit: I should note that I saw the fork for this issue is 1000s of commits behind so probably not a viable replacement for TS 1.7 with targetSupportsPromises: false
@leebenson Your video above shows a good solution, I have one working much the same as this now, without the node5 preset I've manually entered what is supported and what is not for Babel/Node5.
However I cannot use async/await in TS if I target <ES6. But I cannot use another Promise library if I target ES6
@nevercast have you come across discussion here and here?
There is no 'easy' solution at present. You can tell the compiler to use a lib.d.ts
file that you specify by using the --noLib
flag and including a custom lib file in your build. You must maintain that file manually with the mix of ES5/ES6 types you want ambiently defined. Doable but not exactly fun.
Ya, this is the whole reason Babel broke out their plugins like this. I think flags for each feature would be a great start.
@yuit is tackling this
Awesome! Will Granular Targeting
be added to the roadmap?
We are starting with the library first (tracked by https://github.com/Microsoft/TypeScript/issues/494). and already on the road map.
I know this is not a good idea for many other things, but I've got a radical proposal, add a "node" as a target in addition to ES6, I suppose node is so popular target it should be maintained in TypeScript.
@Ciantic I'm not sure that makes sense: es5/es6 contain a fixed list of language features declared by their respective specifications (when finalized.) "node" is a moving target.
The node target idea was already discussed. https://github.com/Microsoft/TypeScript/issues/4692#issuecomment-163557313
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 eitherES3
,ES5
orES6
. 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.:
In summary:
--noLib
and/or manually maintaininglib.b.ts
files brings other problems:CommonJS modules won't compile, even though that's the only module system Node supports.(fixed by #4811)Workarounds
To achieve mixed ES5/ES6 core typings:
--target ES5
and selectively add ES6 typings in separately maintained files (eg from DefinitelyTyped).--target ES6
and be careful to avoid referencing unsupported ES6 features (the compiler won't issue any errors).--noLib
and manually maintain custom core typings in your own project.To use ES6 features supported by the target platform
--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).--target ES6
and (a)convert everything from CommonJS to ES6 modules(fixed by #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 iftarget
is set toES6
or higher.The additional compiler options have the form
targetHasXYZ
, whereXYZ
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 thetarget
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 symbolsThese options work both on the command line and in
tsconfig.json
files.Example
tsconfig.json
Files and their BehaviourA.
Emits ES6 JavaScript, except with CommonJS module syntax, and with
let
/const
down-leveled tovar
. This might match a Node.js environment.B.
Emits ES5 JavaScript, except with Symbol references emitted as-is, and with full type support for well-known symbols from the default lib.
C.
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
#if
and#endif
add new language syntax. No existing language features are affected.lib.es6.d.ts
). It contains many conditionally compiled sections (ie with#if
and#endif
)Remaining Work and Questions
Map
/Set
/WeakMap
/WeakSet
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).