Skip to content
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

Closed
yortus opened this issue Sep 8, 2015 · 51 comments
Closed

Proposal: Granular Targeting #4692

yortus opened this issue Sep 8, 2015 · 51 comments
Labels
Suggestion An idea for TypeScript

Comments

@yortus
Copy link
Contributor

yortus commented Sep 8, 2015

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.:

In summary:

  • The default lib either includes all ES6 types and properties, or none of them.
  • Specifying --noLib and/or manually maintaining lib.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 () => {...} syntax
  • targetHasBlockScoping: specify whether the target supports ES6 let and const
  • targetHasForOf: specify whether the target supports ES6 for..of syntax
  • targetHasGenerators: specify whether the target supports ES6 generators
  • targetHasIterables: specify whether the target supports ES6 iterables and iterators
  • targetHasModules: specify whether the target supports ES6 modules
  • targetHasPromises: specify whether the target supports ES6 promises
  • targetHasSymbols: 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).
@weswigham
Copy link
Member

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.

@Arnavion
Copy link
Contributor

Arnavion commented Sep 8, 2015

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?

  • 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.

@weswigham
Copy link
Member

@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.

@Arnavion
Copy link
Contributor

Arnavion commented Sep 8, 2015

(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.

@weswigham
Copy link
Member

@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)

@danquirk danquirk added the Suggestion An idea for TypeScript label Sep 8, 2015
@LPGhatguy
Copy link
Contributor

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.

@rbuckton
Copy link
Member

rbuckton commented Sep 8, 2015

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.

@yortus
Copy link
Contributor Author

yortus commented Sep 9, 2015

@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.

@yortus
Copy link
Contributor Author

yortus commented Sep 9, 2015

@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.

@yortus
Copy link
Contributor Author

yortus commented Sep 9, 2015

@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)
  • 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)
  • the design time decorators that @rbuckton is working on (also a form of conditional compilation?)
  • having no physical 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)
  • Having many small core lib files for each feature as suggested by @LPGhatguy and elsewhere - but I think this is harder than it sounds due to feature inter-relationships.

@yortus
Copy link
Contributor Author

yortus commented Sep 9, 2015

@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;
}

@weswigham
Copy link
Member

@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.

@yortus
Copy link
Contributor Author

yortus commented Sep 9, 2015

@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?

@yortus
Copy link
Contributor Author

yortus commented Sep 9, 2015

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:

  • (a) if you specify target you achieve brevity but cannot achieve any feature granularity
  • (b) if you don't specify target 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.

@LPGhatguy
Copy link
Contributor

@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
    }
}

@yortus
Copy link
Contributor Author

yortus commented Sep 9, 2015

@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|{...}

@weswigham
Copy link
Member

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 214df64 (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.

@yortus
Copy link
Contributor Author

yortus commented Sep 9, 2015

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 tsconfigs that don't use the new flags.

@yortus
Copy link
Contributor Author

yortus commented Sep 9, 2015

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:

  • one huge lib.d.ts file
  • many separate lib files divided by features, where individual type declarations may be split across several files (eg ssymbols.d.ts, symbols+iterables.lib.ts, promises+iterables.lib.ts etc
  • many separate lib files that keep each type (and its close relatives) in one place (eg Promise, 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.

@kitsonk
Copy link
Contributor

kitsonk commented Sep 9, 2015

Very similar to #3538. 😉 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.

@yortus
Copy link
Contributor Author

yortus commented Sep 9, 2015

@kitsonk runtime is a bit late to apply granular targets - the JS has already been emitted and the types have already been checked.

@kitsonk
Copy link
Contributor

kitsonk commented Sep 9, 2015

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.

@yortus
Copy link
Contributor Author

yortus commented Sep 9, 2015

@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?

@kitsonk
Copy link
Contributor

kitsonk commented Sep 9, 2015

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.

@yortus
Copy link
Contributor Author

yortus commented Sep 9, 2015

@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.

@kitsonk
Copy link
Contributor

kitsonk commented Sep 10, 2015

@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.

@amcdnl
Copy link

amcdnl commented Jan 30, 2016

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
Copy link
Member

@yuit is tackling this

@RyanCavanaugh RyanCavanaugh added Committed The team has roadmapped this issue and removed In Discussion Not yet reached consensus labels Feb 1, 2016
@bootstraponline
Copy link
Contributor

Awesome! Will Granular Targeting be added to the roadmap?

@mhegazy
Copy link
Contributor

mhegazy commented Feb 1, 2016

We are starting with the library first (tracked by #494). and already on the road map.

@Ciantic
Copy link

Ciantic commented Feb 27, 2016

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.

@oising
Copy link

oising commented Mar 1, 2016

@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.

@bootstraponline
Copy link
Contributor

The node target idea was already discussed.
#4692 (comment)

@mhegazy
Copy link
Contributor

mhegazy commented Sep 21, 2016

With the support of --lib flag 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.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests