<p dir="auto">This proposal is based on a working implementation at:<br>
<a href="https://github.com/yortus/TypeScript/tree/granular-targeting">https://github.com/yortus/TypeScript/tree/granular-targeting</a><br>
To try it out, clone it or install it with <code class="notranslate">npm install yortus-typescript</code></p>
<h2 dir="auto">Problem Scenario</h2>
<p dir="auto">The TypeScript compiler accepts a single <code class="notranslate">target</code> option of either <code class="notranslate">ES3</code>, <code class="notranslate">ES5</code> or <code class="notranslate">ES6</code>. 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).</p>
<p dir="auto">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.:</p>
<ul dir="auto">
<li>(<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="102279466" data-permission-text="Title is private" data-url="https://github.com/microsoft/TypeScript/issues/4389" data-hovercard-type="issue" data-hovercard-url="/microsoft/TypeScript/issues/4389/hovercard" href="https://github.com/microsoft/TypeScript/issues/4389">#4389</a>) Support compile targets between ES5 and ES6</li>
<li>(<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="99255652" data-permission-text="Title is private" data-url="https://github.com/microsoft/TypeScript/issues/4168" data-hovercard-type="issue" data-hovercard-url="/microsoft/TypeScript/issues/4168/hovercard" href="https://github.com/microsoft/TypeScript/issues/4168">#4168</a>) Normalize our lib files by compiler settings</li>
<li>(<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="78054310" data-permission-text="Title is private" data-url="https://github.com/microsoft/TypeScript/issues/3215" data-hovercard-type="issue" data-hovercard-url="/microsoft/TypeScript/issues/3215/hovercard" href="https://github.com/microsoft/TypeScript/issues/3215">#3215</a>) New APIs added to lib.d.ts may break client codes. Allow duplicated members in interfaces? Make lib.d.ts overridable?</li>
<li>(<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="72630734" data-permission-text="Title is private" data-url="https://github.com/microsoft/TypeScript/issues/3005" data-hovercard-type="issue" data-hovercard-url="/microsoft/TypeScript/issues/3005/hovercard" href="https://github.com/microsoft/TypeScript/issues/3005">#3005</a>) Using ES6 type default library when targetting ES5 output</li>
<li>(<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="67462688" data-permission-text="Title is private" data-url="https://github.com/microsoft/TypeScript/issues/2695" data-hovercard-type="issue" data-hovercard-url="/microsoft/TypeScript/issues/2695/hovercard" href="https://github.com/microsoft/TypeScript/issues/2695">#2695</a>) for-of does not work with DOM collections when target is ES6</li>
<li>(somewhat related: (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="64038648" data-permission-text="Title is private" data-url="https://github.com/microsoft/TypeScript/issues/2481" data-hovercard-type="issue" data-hovercard-url="/microsoft/TypeScript/issues/2481/hovercard" href="https://github.com/microsoft/TypeScript/issues/2481">#2481</a>) Create DOM-Level specific dom-version.d.ts)</li>
</ul>
<p dir="auto">In summary:</p>
<ul dir="auto">
<li>The default lib either includes all ES6 types and properties, or none of them.</li>
<li>Specifying <code class="notranslate">--noLib</code> and/or manually maintaining <code class="notranslate">lib.b.ts</code> files brings other problems:
<ul dir="auto">
<li>separate core typings are a burden to maintain.</li>
<li>problems of missing symbols and clashing symbols.</li>
<li>burden of manually tracking fixes and additions made in the default libs.</li>
</ul>
</li>
<li>Targeting ES5 as the 'lowest common denominator' means some language features known to be supported cannot be used (eg generators in Node.js).</li>
<li>Targeting ES6 (e.g. to take advantage of Node.js' support for many ES6 features) leads to further complications:
<ul dir="auto">
<li><del>CommonJS modules won't compile, even though that's the only module system Node supports.</del> (fixed by <a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="106663759" data-permission-text="Title is private" data-url="https://github.com/microsoft/TypeScript/issues/4811" data-hovercard-type="pull_request" data-hovercard-url="/microsoft/TypeScript/pull/4811/hovercard" href="https://github.com/microsoft/TypeScript/pull/4811">#4811</a>)</li>
<li>The compiler emits ES6 even for features that are known <em>not</em> to be supported, which would fail at runtime.</li>
<li>Adding babel.js to the build pipeline adds complexity.</li>
</ul>
</li>
</ul>
<h2 dir="auto">Workarounds</h2>
<h4 dir="auto">To achieve mixed ES5/ES6 core typings:</h4>
<ul dir="auto">
<li>specify <code class="notranslate">--target ES5</code> and selectively add ES6 typings in separately maintained files (eg from DefinitelyTyped).</li>
<li>specify <code class="notranslate">--target ES6</code> and be careful to avoid referencing unsupported ES6 features (the compiler won't issue any errors).</li>
<li>specify <code class="notranslate">--noLib</code> and manually maintain custom core typings in your own project.</li>
</ul>
<h4 dir="auto">To use ES6 features supported by the target platform</h4>
<ul dir="auto">
<li>specify <code class="notranslate">--target ES5</code> and (a) accept that things will be down-level emitted, and (b) don't use features with no down-level emit yet (ie generators).</li>
<li>specify <code class="notranslate">--target ES6</code> and (a) <del>convert everything from CommonJS to ES6 modules</del> (fixed by <a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="106663759" data-permission-text="Title is private" data-url="https://github.com/microsoft/TypeScript/issues/4811" data-hovercard-type="pull_request" data-hovercard-url="/microsoft/TypeScript/pull/4811/hovercard" href="https://github.com/microsoft/TypeScript/pull/4811">#4811</a>), (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.</li>
</ul>
<h2 dir="auto">Proposed Solution</h2>
<p dir="auto">This proposal consists of two parts:</p>
<p dir="auto"><del>1. Support for conditional compilation using <code class="notranslate">#if</code> and <code class="notranslate">#endif</code> directives, so that a single default lib can offer fine-grained typings tailored to a mixed ES3/5/6/7 target environment.</del></p>
<p dir="auto"><del>The conditional compilation part is detailed in a separate proposal (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="105411046" data-permission-text="Title is private" data-url="https://github.com/microsoft/TypeScript/issues/4691" data-hovercard-type="issue" data-hovercard-url="/microsoft/TypeScript/issues/4691/hovercard" href="https://github.com/microsoft/TypeScript/issues/4691">#4691</a>) with its own <a href="https://github.com/yortus/TypeScript/tree/preprocessor-directives">working implementation</a>.</del></p>
<p dir="auto"><strong>1. A mechanism allowing the default lib to offer fine-grained typings tailored to a mixed ES3/5/6/7 target environment.</strong></p>
<p dir="auto">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.</p>
<p dir="auto">The <a href="https://github.com/yortus/TypeScript/tree/granular-targeting">working implementation</a> uses <code class="notranslate">#if...#endif</code> conditional compilation proposed in <a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="105411046" data-permission-text="Title is private" data-url="https://github.com/microsoft/TypeScript/issues/4691" data-hovercard-type="issue" data-hovercard-url="/microsoft/TypeScript/issues/4691/hovercard" href="https://github.com/microsoft/TypeScript/issues/4691">#4691</a>. But this is overkill for this use case and <a href="https://github.com/Microsoft/TypeScript/issues/4691#issuecomment-138678473" data-hovercard-type="issue" data-hovercard-url="/microsoft/TypeScript/issues/4691/hovercard">seems unlikely</a> to be considered.</p>
<p dir="auto">Several other mechanisms have been discussed (summarized <a href="https://github.com/Microsoft/TypeScript/issues/4692#issuecomment-138750104" data-hovercard-type="issue" data-hovercard-url="/microsoft/TypeScript/issues/4692/hovercard">here</a>).</p>
<p dir="auto"><strong>2. Support for additional compiler options allowing the target environment to be described on a feature-by-feature basis.</strong></p>
<p dir="auto">Under this proposal, the <code class="notranslate">target</code> 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 <code class="notranslate">target</code> is set to <code class="notranslate">ES6</code> or higher.</p>
<p dir="auto">The additional compiler options have the form <code class="notranslate">targetHasXYZ</code>, where <code class="notranslate">XYZ</code> 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 <code class="notranslate">target</code> option otherwise imples.</p>
<p dir="auto">The <a href="https://github.com/yortus/TypeScript/tree/granular-targeting">working implementation</a> currently supports the following additional compiler options (all boolean):</p>
<ul dir="auto">
<li><code class="notranslate">targetHasArrowFunctions</code>: specify whether the target supports ES6 <code class="notranslate">() =&gt; {...}</code> syntax</li>
<li><code class="notranslate">targetHasBlockScoping</code>: specify whether the target supports ES6 <code class="notranslate">let</code> and <code class="notranslate">const</code></li>
<li><code class="notranslate">targetHasForOf</code>: specify whether the target supports ES6 <code class="notranslate">for..of</code> syntax</li>
<li><code class="notranslate">targetHasGenerators</code>: specify whether the target supports ES6 generators</li>
<li><code class="notranslate">targetHasIterables</code>: specify whether the target supports ES6 iterables and iterators</li>
<li><code class="notranslate">targetHasModules</code>: specify whether the target supports ES6 modules</li>
<li><code class="notranslate">targetHasPromises</code>: specify whether the target supports ES6 promises</li>
<li><code class="notranslate">targetHasSymbols</code>: specify whether the target supports ES6 symbols</li>
</ul>
<p dir="auto">These options work both on the command line and in <code class="notranslate">tsconfig.json</code> files.</p>
<h2 dir="auto">Example <code class="notranslate">tsconfig.json</code> Files and their Behaviour</h2>
<h4 dir="auto">A.</h4>
<div class="highlight highlight-source-json notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="{
    &quot;target&quot;: &quot;es6&quot;,
    &quot;targetHasModules&quot;: false,
    &quot;targetHasBlockScoping&quot;: false,
    &quot;module&quot;: &quot;commonjs&quot;
}"><pre class="notranslate">{
    <span class="pl-ent">"target"</span>: <span class="pl-s"><span class="pl-pds">"</span>es6<span class="pl-pds">"</span></span>,
    <span class="pl-ent">"targetHasModules"</span>: <span class="pl-c1">false</span>,
    <span class="pl-ent">"targetHasBlockScoping"</span>: <span class="pl-c1">false</span>,
    <span class="pl-ent">"module"</span>: <span class="pl-s"><span class="pl-pds">"</span>commonjs<span class="pl-pds">"</span></span>
}</pre></div>
<p dir="auto">Emits ES6 JavaScript, except with CommonJS module syntax, and with <code class="notranslate">let</code>/<code class="notranslate">const</code> down-leveled to <code class="notranslate">var</code>. This might match a Node.js environment.</p>
<h4 dir="auto">B.</h4>
<div class="highlight highlight-source-json notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="{
    &quot;target&quot;: &quot;es5&quot;,
    &quot;targetHasSymbols&quot;: true
}"><pre class="notranslate">{
    <span class="pl-ent">"target"</span>: <span class="pl-s"><span class="pl-pds">"</span>es5<span class="pl-pds">"</span></span>,
    <span class="pl-ent">"targetHasSymbols"</span>: <span class="pl-c1">true</span>
}</pre></div>
<p dir="auto">Emits ES5 JavaScript, except with Symbol references emitted as-is, and with full type support for well-known symbols from the default lib.</p>
<h4 dir="auto">C.</h4>
<div class="highlight highlight-source-json notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="{
    &quot;target&quot;: &quot;es5&quot;,
    &quot;targetHasPromises&quot;: true
}"><pre class="notranslate">{
    <span class="pl-ent">"target"</span>: <span class="pl-s"><span class="pl-pds">"</span>es5<span class="pl-pds">"</span></span>,
    <span class="pl-ent">"targetHasPromises"</span>: <span class="pl-c1">true</span>
}</pre></div>
<p dir="auto">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 <code class="notranslate">Promise</code> object.</p>
<h2 dir="auto">Backward Compatibility, Design Impact, Performance, etc</h2>
<ul dir="auto">
<li>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.</li>
<li>The preprocessor directives <code class="notranslate">#if</code> and <code class="notranslate">#endif</code> add new language syntax. No existing language features are affected.</li>
<li>There is negligable impact on compiler performance.</li>
<li>Only one default lib is needed (<code class="notranslate">lib.es6.d.ts</code>). It contains many conditionally compiled sections (ie with <code class="notranslate">#if</code> and <code class="notranslate">#endif</code>)</li>
</ul>
<h2 dir="auto">Remaining Work and Questions</h2>
<ul dir="auto">
<li>Support compiler options for more target features, e.g.:
<ul dir="auto">
<li>template strings</li>
<li>classes</li>
<li><code class="notranslate">Map</code>/<code class="notranslate">Set</code>/<code class="notranslate">WeakMap</code>/<code class="notranslate">WeakSet</code></li>
<li>binary and octal literals</li>
<li>destructuring</li>
<li>default, rest, and optional parameters</li>
</ul>
</li>
<li>How granular could/should targets be? Feature support is naturally hierarchical. E.g. block scoping may be separated into (a) <code class="notranslate">let</code>, (b) <code class="notranslate">const</code> and (c) block-level function declaration. This is true of most features and their realistic implementations (the <a href="https://kangax.github.io/compat-table/es6/" rel="nofollow">Kangax ES6 compatibility table</a> has a three-level hierarchy down the left side).</li>
</ul>
<p dir="auto">I have the below overloaded function declaration for a function in my class</p>
<div class="highlight highlight-source-ts notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="private _forEachBindingContext(bindingContext: IContext, fn: (bindingContext: IContext) =&gt; void);
private _forEachBindingContext(bindingContextArray: Array&lt;IContext&gt;, fn: (bindingContext:IContext) =&gt; void);
private _forEachBindingContext(context, fn: (bindingContext: IContext) =&gt; void): void {
    // Function here
}"><pre class="notranslate"><span class="pl-k">private</span> <span class="pl-en">_forEachBindingContext</span><span class="pl-kos">(</span><span class="pl-s1">bindingContext</span>: <span class="pl-smi">IContext</span><span class="pl-kos">,</span> <span class="pl-en">fn</span>: <span class="pl-kos">(</span><span class="pl-s1">bindingContext</span>: <span class="pl-smi">IContext</span><span class="pl-kos">)</span> <span class="pl-c1">=</span><span class="pl-c1">&gt;</span> <span class="pl-k">void</span><span class="pl-s1"></span><span class="pl-kos">)</span><span class="pl-kos">;</span>
<span class="pl-s1">private</span><span class="pl-kos"></span> <span class="pl-en">_forEachBindingContext</span><span class="pl-kos">(</span><span class="pl-s1">bindingContextArray</span>: <span class="pl-smi">Array</span><span class="pl-kos">&lt;</span><span class="pl-smi">IContext</span><span class="pl-kos">&gt;</span><span class="pl-kos">,</span> <span class="pl-en">fn</span>: <span class="pl-kos">(</span><span class="pl-s1">bindingContext</span>:<span class="pl-smi">IContext</span><span class="pl-kos">)</span> <span class="pl-c1">=</span><span class="pl-c1">&gt;</span> <span class="pl-k">void</span><span class="pl-s1"></span><span class="pl-kos">)</span><span class="pl-kos">;</span>
<span class="pl-s1">private</span><span class="pl-kos"></span> <span class="pl-s1">_forEachBindingContext</span><span class="pl-kos">(</span><span class="pl-s1">context</span><span class="pl-kos">,</span> <span class="pl-en">fn</span>: <span class="pl-kos">(</span><span class="pl-s1">bindingContext</span>: <span class="pl-smi">IContext</span><span class="pl-kos">)</span> <span class="pl-c1">=</span><span class="pl-c1">&gt;</span> <span class="pl-k">void</span><span class="pl-kos">)</span>: <span class="pl-k">void</span> <span class="pl-kos">{</span>
    <span class="pl-c">// Function here</span>
<span class="pl-kos">}</span></pre></div>
<p dir="auto">In previous versions of TypeScript to 1.1 it would generate a definition file with the line</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="private _forEachBindingContext(bindingContext, fn);"><pre lang="typesript" class="notranslate"><code class="notranslate">private _forEachBindingContext(bindingContext, fn);
</code></pre></div>
<p dir="auto">I could use this definition file in my other projects and it would work fine. After upgrading to 1.1 the same code now generates the following in the definition file</p>
<div class="highlight highlight-source-ts notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="        private _forEachBindingContext(bindingContext, fn);
        private _forEachBindingContext(bindingContextArray, fn);"><pre class="notranslate">        <span class="pl-k">private</span> <span class="pl-en">_forEachBindingContext</span><span class="pl-kos">(</span><span class="pl-s1">bindingContext</span><span class="pl-kos">,</span> <span class="pl-s1">fn</span><span class="pl-kos">)</span><span class="pl-kos">;</span>
        <span class="pl-s1">private</span><span class="pl-kos"></span> <span class="pl-en">_forEachBindingContext</span><span class="pl-kos">(</span><span class="pl-s1">bindingContextArray</span><span class="pl-kos">,</span> <span class="pl-s1">fn</span><span class="pl-kos">)</span><span class="pl-kos">;</span></pre></div>
<p dir="auto">If I try using this definition file in other projects I get the following error:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="Duplicate overload signature for '_forEachBindingContext'."><pre class="notranslate"><code class="notranslate">Duplicate overload signature for '_forEachBindingContext'.
</code></pre></div>
<p dir="auto">Changing this method to public solves the issue since it generates types on the definitions of public functions. I would expect that I could use either public or private and not encounter this issue.</p>