-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: Granular Targeting #4692
Comments
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: {
"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 I'd rather like see the old 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, 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 Beyond that, it may be acceptable to include some kind of feature-dependency pragma within a triple-slash comment in an entire |
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 |
Eg: Say TS 1.7 gets released with support for the proposed bind operator |
@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. |
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 |
In the past we discussed pre-processor directives like We have been considering future support for "design-time" decorators, which only affect the compiler (and would not be written to the output file):
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 |
@Arnavion regarding default values: under this proposal if a particular option is not explicitly given, the default value is neither 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 As is already the case, if you don't specify a Alternatively if you set |
@weswigham @LPGhatguy there no need for this proposal to be wedded to the Some solutions to this:
|
If I understand correctly, this would work at compile time and in ambient contexts like 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;
} |
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.
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.
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): ///<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;
///<feature provides="symbols" requires="computedProperties promises">
interface Promise<T> {
[Symbol.toStringTag]: string;
}
interface PromiseConstructor {
[Symbol.species]: Promise<any>;
}
//...
///<feature provides="symbols" requires="computedProperties iterables">
interface SymbolConstructor {
isConcatSpreadable: symbol;
iterator: symbol;
}
//... (As a trimmed down example. Interface merging makes it all pretty nice.) IMO, avoiding any more |
@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:
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 Point taken about getting the proposal right with regard to using sub-hashes in the |
That's effectively how the proposal works, except that they are not mutually exclusive. The if (languageVersion >= ScriptTarget.ES6 && node.asteriskToken) {
write("*");
} it looks like: if (languageVersion.hasGenerators && node.asteriskToken) {
write("*");
} What I don't like about making
With the current proposal you can be both concise and deterministic: {
"target": "es5",
"targetHasPromises": true
} It's clear here what all the omitted |
@yortus Potentially instead of 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 |
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, /rant
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, Anyways, there's a another what-does-target-mean issue here, though. At present, {
"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 |
That's why I wouldn't be keen on an option like |
One advantage of conditionally compiling declarations in
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. 😉 and some of the commentary there I suspect would apply here as well. Personally, I think full static resolution in |
@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 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 console.log('I Promise to always love you'); And if 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. |
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 |
We are starting with the library first (tracked by #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. |
With the support of the other part is about transformations being picked up a la cart, is something we would not consider in the time being. supporting an ES Next mode however is still something we are interested in. |
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 Support modules when targeting ES6 and an ES6 ModuleKind #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 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 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).The text was updated successfully, but these errors were encountered: