<p dir="auto">Relates to <a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="76178424" data-permission-text="Title is private" data-url="https://github.com/microsoft/TypeScript/issues/3159" data-hovercard-type="pull_request" data-hovercard-url="/microsoft/TypeScript/pull/3159/hovercard" href="https://github.com/microsoft/TypeScript/pull/3159">#3159</a>, <a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="97865484" data-permission-text="Title is private" data-url="https://github.com/microsoft/TypeScript/issues/4068" data-hovercard-type="pull_request" data-hovercard-url="/microsoft/TypeScript/pull/4068/hovercard" href="https://github.com/microsoft/TypeScript/pull/4068">#4068</a>, <a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="65547883" data-permission-text="Title is private" data-url="https://github.com/microsoft/TypeScript/issues/2568" data-hovercard-type="issue" data-hovercard-url="/microsoft/TypeScript/issues/2568/hovercard" href="https://github.com/microsoft/TypeScript/issues/2568">#2568</a>, <a href="https://github.com/Microsoft/TypeScript/compare/declarations...declarations-moduleOut">this branch</a>, <a href="https://github.com/SitePen/dts-generator">and this tool</a>.</p>
<h1 dir="auto">Goals</h1>
<ul dir="auto">
<li>Bundle <em>declarations</em> for TS projects to allow a library to be consumed with a single TS file, despite being many modules internally. Because of complicated internal module dependencies which would rather not be exposed to the consumer, this should flatten the exported module types as best as it can. (Ideally, completely.)</li>
</ul>
<h1 dir="auto">Proposal</h1>
<p dir="auto">When all of <code class="notranslate">--module</code>, <code class="notranslate">--out</code>, and <code class="notranslate">--declarations</code> are specified, the TS compiler should emit a single amalgamated <code class="notranslate">.d.ts</code> (alongside its single output js file). This <code class="notranslate">.d.ts</code> should be flattened compared to a concatenated <code class="notranslate">.d.ts</code> file. It should report collisions caused by scoping issues and import aliasing when flattening declarations into a single <code class="notranslate">declare module</code>. It should respect access modifiers when generating the DTS (only exporting things explicitly exported and types marked as public).</p>
<p dir="auto">For example, given the following set of sources:<br>
<code class="notranslate">tsconfig.json</code>:</p>
<div class="highlight highlight-source-json notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="{
  &quot;compilerOptions&quot;: {
    &quot;module&quot;: &quot;commonjs&quot;,
    &quot;declarations&quot;: true,
    &quot;out&quot;: &quot;mylib.js&quot;
  }
}"><pre class="notranslate">{
  <span class="pl-ent">"compilerOptions"</span>: {
    <span class="pl-ent">"module"</span>: <span class="pl-s"><span class="pl-pds">"</span>commonjs<span class="pl-pds">"</span></span>,
    <span class="pl-ent">"declarations"</span>: <span class="pl-c1">true</span>,
    <span class="pl-ent">"out"</span>: <span class="pl-s"><span class="pl-pds">"</span>mylib.js<span class="pl-pds">"</span></span>
  }
}</pre></div>
<p dir="auto"><code class="notranslate">a.ts</code>:</p>
<div class="highlight highlight-source-ts notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="export * from './b';
export * from './c';"><pre class="notranslate"><span class="pl-k">export</span> <span class="pl-c1">*</span> <span class="pl-k">from</span> <span class="pl-s">'./b'</span><span class="pl-kos">;</span>
<span class="pl-k">export</span> <span class="pl-c1">*</span> <span class="pl-k">from</span> <span class="pl-s">'./c'</span><span class="pl-kos">;</span></pre></div>
<p dir="auto"><code class="notranslate">b.ts</code>:</p>
<div class="highlight highlight-source-ts notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="export interface Foo {}

export class Bar {
    constructor() {
        console.log('');
    }

    do(): Foo { throw new Error('Not implemented.'); }
}"><pre class="notranslate"><span class="pl-k">export</span> <span class="pl-k">interface</span> <span class="pl-smi">Foo</span> <span class="pl-kos">{</span><span class="pl-kos">}</span>

<span class="pl-k">export</span> <span class="pl-k">class</span> <span class="pl-smi">Bar</span> <span class="pl-kos">{</span>
    <span class="pl-en">constructor</span><span class="pl-kos">(</span><span class="pl-kos">)</span> <span class="pl-kos">{</span>
        <span class="pl-smi">console</span><span class="pl-kos">.</span><span class="pl-en">log</span><span class="pl-kos">(</span><span class="pl-s">''</span><span class="pl-kos">)</span><span class="pl-kos">;</span>
    <span class="pl-kos">}</span>

    <span class="pl-en">do</span><span class="pl-kos">(</span><span class="pl-kos">)</span>: <span class="pl-smi">Foo</span> <span class="pl-kos">{</span> <span class="pl-k">throw</span> <span class="pl-k">new</span> <span class="pl-smi">Error</span><span class="pl-kos">(</span><span class="pl-s">'Not implemented.'</span><span class="pl-kos">)</span><span class="pl-kos">;</span> <span class="pl-kos">}</span>
<span class="pl-kos">}</span></pre></div>
<p dir="auto"><code class="notranslate">c.ts</code>:</p>
<div class="highlight highlight-source-ts notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="export class Baz {}"><pre class="notranslate"><span class="pl-k">export</span> <span class="pl-k">class</span> <span class="pl-smi">Baz</span> <span class="pl-kos">{</span><span class="pl-kos">}</span></pre></div>
<p dir="auto">should create the <code class="notranslate">.d.ts</code>:<br>
<code class="notranslate">mylib.d.ts</code>:</p>
<div class="highlight highlight-source-ts notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="declare module &quot;mylib&quot; {
  export interface Foo {}

  export class Bar {
    constructor()
    do(): Foo
  }

  export class Baz {}
}"><pre class="notranslate"><span class="pl-k">declare</span> module <span class="pl-s">"mylib"</span> <span class="pl-kos">{</span>
  <span class="pl-k">export</span> <span class="pl-k">interface</span> <span class="pl-smi">Foo</span> <span class="pl-kos">{</span><span class="pl-kos">}</span>

  <span class="pl-k">export</span> <span class="pl-k">class</span> <span class="pl-smi">Bar</span> <span class="pl-kos">{</span>
    <span class="pl-c1">constructor</span><span class="pl-kos">(</span><span class="pl-kos">)</span>
    <span class="pl-c1">do</span><span class="pl-kos">(</span><span class="pl-kos">)</span>: <span class="pl-smi">Foo</span>
  <span class="pl-kos">}</span>

  <span class="pl-k">export</span> <span class="pl-k">class</span> <span class="pl-smi">Baz</span> <span class="pl-kos">{</span><span class="pl-kos">}</span>
<span class="pl-kos">}</span></pre></div>
<p dir="auto">rather than:<br>
<code class="notranslate">mylib.d.ts</code>:</p>
<div class="highlight highlight-source-ts notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="declare module &quot;mylib/a&quot; {
  export * from &quot;mylib/b&quot;;
  export * from &quot;mylib/c&quot;;
}
declare module &quot;mylib/b&quot; {
  export interface Foo {}

  export class Bar {
    constructor()
    do(): Foo
  }
}
declare module &quot;mylib/c&quot; {
  export class Baz {}
}
declare module &quot;mylib&quot; {
  export * from &quot;mylib/a&quot;;
}"><pre class="notranslate"><span class="pl-k">declare</span> module <span class="pl-s">"mylib/a"</span> <span class="pl-kos">{</span>
  <span class="pl-k">export</span> <span class="pl-c1">*</span> <span class="pl-k">from</span> <span class="pl-s">"mylib/b"</span><span class="pl-kos">;</span>
  <span class="pl-k">export</span> <span class="pl-c1">*</span> <span class="pl-k">from</span> <span class="pl-s">"mylib/c"</span><span class="pl-kos">;</span>
<span class="pl-kos">}</span>
<span class="pl-k">declare</span> module <span class="pl-s">"mylib/b"</span> <span class="pl-kos">{</span>
  <span class="pl-k">export</span> <span class="pl-k">interface</span> <span class="pl-smi">Foo</span> <span class="pl-kos">{</span><span class="pl-kos">}</span>

  <span class="pl-k">export</span> <span class="pl-k">class</span> <span class="pl-smi">Bar</span> <span class="pl-kos">{</span>
    <span class="pl-c1">constructor</span><span class="pl-kos">(</span><span class="pl-kos">)</span>
    <span class="pl-c1">do</span><span class="pl-kos">(</span><span class="pl-kos">)</span>: <span class="pl-smi">Foo</span>
  <span class="pl-kos">}</span>
<span class="pl-kos">}</span>
<span class="pl-k">declare</span> module <span class="pl-s">"mylib/c"</span> <span class="pl-kos">{</span>
  <span class="pl-k">export</span> <span class="pl-k">class</span> <span class="pl-smi">Baz</span> <span class="pl-kos">{</span><span class="pl-kos">}</span>
<span class="pl-kos">}</span>
<span class="pl-k">declare</span> module <span class="pl-s">"mylib"</span> <span class="pl-kos">{</span>
  <span class="pl-k">export</span> <span class="pl-c1">*</span> <span class="pl-k">from</span> <span class="pl-s">"mylib/a"</span><span class="pl-kos">;</span>
<span class="pl-kos">}</span></pre></div>
<p dir="auto">and should report a semantic error when the following is done:<br>
<code class="notranslate">a.ts</code>:</p>
<div class="highlight highlight-source-ts notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="export * from './b';
export {Bar as Foo} from './b';
export * from './c';"><pre class="notranslate"><span class="pl-k">export</span> <span class="pl-c1">*</span> <span class="pl-k">from</span> <span class="pl-s">'./b'</span><span class="pl-kos">;</span>
<span class="pl-k">export</span> <span class="pl-kos">{</span><span class="pl-smi">Bar</span> <span class="pl-k">as</span> <span class="pl-smi">Foo</span><span class="pl-kos">}</span> <span class="pl-k">from</span> <span class="pl-s">'./b'</span><span class="pl-kos">;</span>
<span class="pl-k">export</span> <span class="pl-c1">*</span> <span class="pl-k">from</span> <span class="pl-s">'./c'</span><span class="pl-kos">;</span></pre></div>
<p dir="auto">as there will be multiple members named <code class="notranslate">Foo</code> (an interface and a class), since <code class="notranslate">b.ts</code> has exported interface <code class="notranslate">Foo</code>.</p>
<p dir="auto">We should also have a semantic error when the following is changed from the original:<br>
If we change <code class="notranslate">c.ts</code>:</p>
<div class="highlight highlight-source-ts notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="export class Baz {}
export interface Foo {}"><pre class="notranslate"><span class="pl-k">export</span> <span class="pl-k">class</span> <span class="pl-smi">Baz</span> <span class="pl-kos">{</span><span class="pl-kos">}</span>
<span class="pl-k">export</span> <span class="pl-k">interface</span> <span class="pl-smi">Foo</span> <span class="pl-kos">{</span><span class="pl-kos">}</span></pre></div>
<p dir="auto">it should be an error in <code class="notranslate">a.ts</code> (since it's blanket exporting <code class="notranslate">b</code> and <code class="notranslate">c</code>), and the error should suggest to alias either <code class="notranslate">c.ts</code>'s <code class="notranslate">Foo</code> or <code class="notranslate">b.ts</code>'s <code class="notranslate">Foo</code> (or both) when reexporting them in <code class="notranslate">a</code>.</p>
<p dir="auto">Internally, when flattening this aliasing becomes important - we need to track usages of the two original <code class="notranslate">Foo</code>'s across the generated <code class="notranslate">.d.ts</code> and rename it to the alias created when it is reexported.</p>
<p dir="auto">Unfortunately, to maintain ES6 compatability, while we can warn about this behavior with classes (since it's possible that a developer is unaware they're overriding a prior export), we still need to support it (or do we? The spec leads me to believe that attempting to export multiple members with the same name - even via <code class="notranslate">export *</code> - is an early syntax error). So it would be nice to have a compiler flag to mark the same kind of thing with classes (or namespaces) as an error, but also do the following by default:</p>
<p dir="auto">We can do automatic name collision resolution, but that can result in unpredictable (or convention-based) public member names... but it must be done, I suppose. We could ignore reexported types since it's appropriate to do so in ES6 (following <code class="notranslate">export *</code> declarations can override previously defined members? maybe? system works this way at present - but that may just be system relying on transpiler implementers to maintain ES6 semantics), then we would need to create "shadowed" types at the appropriate level in the <code class="notranslate">.d.ts</code> - types whose original public access are overridden by later exports but whose types are still required to describe public function argument or return types. Naming these "shadowed" types could be difficult, but given that they only exist for type information and not for access information, a common (re)naming convention could be a desirable solution. Something akin to <code class="notranslate">&lt;typename&gt;_n</code> when <code class="notranslate">n</code> is the shadowed type number for that type, and renaming the shadowed type name to something else (<code class="notranslate">&lt;typename&gt;__n</code> and so on so long as the name still exists) if that collides with another exported type. Classes used in this way are rewritten to interfaces in the <code class="notranslate">.d.ts</code>, since a constructor function likely isn't accessible for a shadowed class (at least not at its generated exported type name).</p>
<p dir="auto">Any feedback? There's a few alternatives to what I've suggested here, which is possibly the most conservative approach in terms of ability to error early but supporting ES6 semantics best. It's possible to silently ignore <code class="notranslate">interface</code> name collisions and rename those automatically as well, but since they're TS constructs and not ES6, I think it's okay to force more discipline in their usage.</p>
<p dir="auto">Something I've been considering is also rewriting <code class="notranslate">namespace</code>s as <code class="notranslate">interface</code>s in the generated <code class="notranslate">.d.ts</code> in this way to further flatten/unify the types, but this... might? not strictly be needed. I haven't come up with a strong case for it.</p>
<div class="highlight highlight-source-ts notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="class C {
  constructor(options?: number) {
    var options = (options || 0);
    // !!! error TS2687: All declarations of 'options' must have identical modifiers.
  }
}"><pre class="notranslate"><span class="pl-k">class</span> <span class="pl-smi">C</span> <span class="pl-kos">{</span>
  <span class="pl-en">constructor</span><span class="pl-kos">(</span><span class="pl-s1">options</span>?: <span class="pl-smi">number</span><span class="pl-kos">)</span> <span class="pl-kos">{</span>
    <span class="pl-k">var</span> <span class="pl-s1">options</span> <span class="pl-c1">=</span> <span class="pl-kos">(</span><span class="pl-s1">options</span> <span class="pl-c1">||</span> <span class="pl-c1">0</span><span class="pl-kos">)</span><span class="pl-kos">;</span>
    <span class="pl-c">// !!! error TS2687: All declarations of 'options' must have identical modifiers.</span>
  <span class="pl-kos">}</span>
<span class="pl-kos">}</span></pre></div>