Repository navigation
Proposal: Granular Targeting #4692
Description
Activity
First off, I think Ron Buckton (@rbuckton) has done/is doing some somewhat related work to this in a branch (along with other refactoring). So ping Ron Buckton (@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 liketarget: { "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
truecan additionally be a configuration object for the feature (as"modules"above, which could deprecate the top-level "module" flag), for example, if the runtime supportsletbut notconst, it could be indicated there. (Otherwise, if having configurable features seems wrong, it could simply be an array of strings, like babel'swhitelistargument.)I'd rather like see the old
targetsyntax 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 thetarget: stringstyle 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 forScriptVersion.ES6and themoduleflag 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.tsparts for each, and parts which are dependent on other features to be included. Just like how.es6.d.tsis conditionally included by target right now - Meaning we'd specifylib.d.tscomponent flag dependencies in code in the compiler, rather than in the.d.tsfile 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[]vsArray<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 likeVM"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.Reacted by Victor Ivens PereiraI agree with Wesley Wigham (@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?
- If true, since I cannot list future feature-flags in my project's current tsconfig.json, so by upgrading the compiler I would be opting in to new features that may not be available on my target platforms.
- If false, when would they start defaulting to true? One minor release later? One major release later?
- Perhaps they wouldn't default to any value. The compiler could require that all feature flags must be explicitly specified, and error out if they're not / prompt at the CLI for the new values. This does make it harder for someone using the CLI without a tsconfig.json, since their commandline would grow longer and longer with every new feature.
Arnav Singh (@Arnavion) Likely
falseunless 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 emptytsconfigwould likely start astrue. 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)
falseis 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.Arnav Singh (@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)LPGhatguy commented
on Sep 8, 2015 ContributorMore actionsI 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.tsfile, there should just be extra.d.tsfiles included with each feature level.In the past we discussed pre-processor directives like
#ifand 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.
Reacted by Maxime QuandalleWesley Wigham (@weswigham) Arnav Singh (@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 andtsconfig.jsonfiles. 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 astsconfig.json.Arnav Singh (@Arnavion) regarding default values: under this proposal if a particular option is not explicitly given, the default value is neither
truenorfalse; it is determined by thetargetoption, which is eitherES3,ES5, orES6. So for example, if your project does not explicitly specify an option fortargetHasPromises, then the compiler will look at thetargetoption. If that is >= ES6, then it will be as if you had specifiedtargetHasPromises: true, otherwise, it would be as if you had specifiedtargetHasPromises: false.This means there is no danger of implicitly changing options when you upgrade the compiler.
Wesley Wigham (@weswigham) this is also a good reason for keeping (ie not deprecating) the
targetoption. It provides a succinct baseline that imples the value for all the othertargetHas...options if they are not explicitly overridden.As is already the case, if you don't specify a
target, the compiler picksES3for you. So a blanktsconfig.jsonfile would implytarget: ES3(that is the current compiler behaviour), and hencefalsefor all the othertargetHas...options.Alternatively if you set
target: ES6and nothing else, you would gettruefor all the ES6targetHas...options, andfalsefor any ES7+targetHas...options.Wesley Wigham (@weswigham) Lucien Greathouse (@LPGhatguy) there no need for this proposal to be wedded to the
#if...#endifproposal. The important thing is to get just the core typings needed for the target features specified.Some solutions to this:
#if...#endifdirectives as is currently proposed (probably overkill since we just need conditional inclusion just in the core lib files)- one big
lib.d.tswith triple-slash pragmas for conditionally including parts of the fileas Wesley Wigham (@weswigham) suggested(effectively a form of conditional compilation) - the design time decorators that Ron Buckton (@rbuckton) is working on (also a form of conditional compilation?)
- having no physical
lib.d.tsfile but have the compiler internally piece together the definitions it needs somehow, as suggested by Wesley Wigham (@weswigham) (although I suspect the cleanest way to implement this would probably involve a physical file with conditional pragmas behind the scenes) - Having many small core lib files for each feature as suggested by Lucien Greathouse (@LPGhatguy) and elsewhere - but I think this is harder than it sounds due to feature inter-relationships.
@Aslan Mammadrzayev (@Profile)("dom") - The decorated member's type information is only visible when the "dom" profile is selected. Other examples could include: @Aslan Mammadrzayev (@Profile)("es6"), @Aslan Mammadrzayev (@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; }
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 Wesley Wigham (@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 whichprovidesthat feature are loaded where theirrequiresare 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 andtsconfig.jsonexist to specify file dependencies nowadays.Wesley Wigham (@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.tsI'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
///featurepragma 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 atcommandLineParser.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
targetoption does effectively expand into a set of feature flags, and thetargetHas...options just optionally override those. It actually makes thechecker.tsandemitter.tscode 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
targetmutually exclusive with the feature-specific options, is that:- (a) if you specify
targetyou achieve brevity but cannot achieve any feature granularity - (b) if you don't specify
targetyou 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
targetHasoptions will be - they will betrueif they are <= ES5 features, andfalseif they are >= ES6 features. And that won't change when building with a different tsc version.- (a) if you specify
30 remaining items
Ya, this is the whole reason Babel broke out their plugins like this. I think flags for each feature would be a great start.
RyanCavanaugh commented
on Feb 1, 2016 MemberMore actionsYui (@yuit) is tackling this
- addedCommittedThe team has roadmapped this issueThe team has roadmapped this issueand removedIn DiscussionNot yet reached consensusNot yet reached consensus
on Feb 1, 2016 Awesome! Will
Granular Targetingbe added to the roadmap?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.
Jari Pennanen (@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.
#4692 (comment)- removedCommittedThe team has roadmapped this issueThe team has roadmapped this issue
on Sep 20, 2016 With the support of
--libflag and breaking up the library in multiple parts, most of this request has been addressed.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.
Reacted by Clayton Watts and AngularNinja.com- locked and limited conversation to collaborators
on Jun 19, 2018
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-typescriptProblem Scenario
The TypeScript compiler accepts a single
targetoption of eitherES3,ES5orES6. 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:
--noLiband/or manually maintaininglib.b.tsfiles 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 ES5and selectively add ES6 typings in separately maintained files (eg from DefinitelyTyped).--target ES6and be careful to avoid referencing unsupported ES6 features (the compiler won't issue any errors).--noLiband manually maintain custom core typings in your own project.To use ES6 features supported by the target platform
--target ES5and (a) accept that things will be down-level emitted, and (b) don't use features with no down-level emit yet (ie generators).--target ES6and (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#ifand#endifdirectives, 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...#endifconditional 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
targetoption 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 iftargetis set toES6or higher.The additional compiler options have the form
targetHasXYZ, whereXYZdesignates 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 thetargetoption 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 ES6letandconsttargetHasForOf: specify whether the target supports ES6for..ofsyntaxtargetHasGenerators: 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.jsonfiles.Example
tsconfig.jsonFiles and their BehaviourA.
{ "target": "es6", "targetHasModules": false, "targetHasBlockScoping": false, "module": "commonjs" }Emits ES6 JavaScript, except with CommonJS module syntax, and with
let/constdown-leveled tovar. 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
Promiseobject.Backward Compatibility, Design Impact, Performance, etc
#ifand#endifadd new language syntax. No existing language features are affected.lib.es6.d.ts). It contains many conditionally compiled sections (ie with#ifand#endif)Remaining Work and Questions
Map/Set/WeakMap/WeakSetlet, (b)constand (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).