Skip to content

Can not declaration merging for default exported class #14080

Closed

Description

TypeScript Version: 2.1.5

Code

// A *self-contained* demonstration of the problem follows...
// ===== file Foo.ts =====
export default class Foo {
  hello() { 
    console.log('Foo'); 
  }
}


// ===== file Bar.ts =====
import Foo from './Foo';

declare module './Foo' {
  interface Foo {
    add(x, y);
  }
}

// ERROR: 'add' does not exist in type Foo.
Foo.prototype.add = function (x, y) { return x + y; };

let f = new Foo();
f.hello();
f.add(3, 4);   // ERROR: 'add' does not exist in type Foo.

Expected behavior:
If the class Foo is exported without default in file Foo.ts, and then import {Foo} from './Foo',
it works correctly as example in doc http://www.typescriptlang.org/docs/handbook/declaration-merging.html

Hope import Foo same as import {Foo} behaviors.

Actual behavior:
ERROR message showed.

Activity

  1. liujunxing commented on Feb 15, 2017

    @liujunxing
    Author

    If I write both export Foo and export default Foo in Foo.ts, its works (both import Foo and import {Foo} works)

    // File Foo.ts
    
    export class Foo { ... }
    export default Foo;
    
  2. mhegazy commented on Feb 15, 2017

    @mhegazy
    Contributor

    You can only augment "exported" declarations. Class Foo is exported as default, and not as Foo. so the name Foo does not exist outside the module.

    It just happens that default is a reserved word, and can not used as an interface declaration name.

    The TS compiler needs to allow export { Foo as default} in module augmentation.

  3. added this to the milestone on Feb 15, 2017
  4. Zaibot commented on Mar 10, 2017

    @Zaibot

    Could this be related to an old bug? #2537

  5. mhegazy commented on Mar 10, 2017

    @mhegazy
    Contributor

    Tobias de Groen (@Zaibot) this feature (module augmentation) did not exist back then. So no.

  6. Zaibot commented on Mar 13, 2017

    @Zaibot

    Sorry, misread or mixed up the thread

  7. jtlapp commented on Sep 28, 2017

    @jtlapp

    Would fixing this address my problem with defining inner classes and interfaces on a default export? I get "Merged declaration 'Outer' cannot include a default export declaration."

    mymodule.ts:

    export default class Outer {
        //...
    }
    
    namespace Outer {
        export class Inner {
            //...
        }
    }
    

    myconsumer.ts:

    import Outer from 'mymodule';
    
    function whatevs(innie: Outer.Inner) {
        //...
    }
    

    I'm trying to keep my consumer code succinct, so I don't have code like:

    import MetaOuter from 'mymodule';
    
    function whatevs(outie: MetaOuter.Outer) {
        //...
    }
    
    function whatevs(innie: MetaOuter.Inner) {
        //...
    }
    

    (Actually, my preference is to do this with an inner interface in my particular situation, but I can't get it to work with an inner class either.)

    (Also, in my case, static methods on subclasses of Outer produce the Inner instances, for use by external classes in characterizing instances of the Outer subclasses.)

  8. jtlapp commented on Sep 28, 2017

    @jtlapp

    My interim solution is to move the inner class out to its own module. I can give it a reasonable import name and still keep class references succinct. I guess I don't really need inner classes and interfaces.

  9. jtlapp commented on Sep 28, 2017

    @jtlapp

    P.S. How did I automatically unassign Yui (@yuit)?

  10. jtlapp commented on Sep 28, 2017

    @jtlapp

    Ugh. Here's a prevalent use case for nested interfaces:

    export default class MyController {
        constructor(options?: Options) {
            //...
        }
    }
    
    namespace MyController {
        export interface Options {
            //...
        }
    }
    

    Except you can't do it yet on default exports.

    Are people using a different pattern? Just not using default exports?

  11. nevir commented on Sep 29, 2017

    @nevir
  12. jtlapp commented on Sep 29, 2017

    @jtlapp

    Thanks Ian MacLeod (@nevir). Is the issue with defaults and refactoring that a class is less likely to have a single name across the entire application? Is that also an argument for not exporting functions directly but instead making stateless functions static methods on exported classes?

  13. 1 remaining item

  14. mattmccutchen commented on Sep 17, 2018

    @mattmccutchen
    Contributor

    I wanted to point out that I think export-assigned (export =) classes have the same problem as default-exported classes of not being augmentable. There have been two questions (1, 2) about this on Stack Overflow in the past week. Given the lack of action on this bug, I didn't think it would be helpful to file a separate bug for export-assigned classes.

  15. cshaa commented on Oct 7, 2018

    @cshaa

    Is going to get fixed anytime soon? It makes correctly typing some modules, eg. markdown-it-incremental-dom, impossible. Markdown-it is an extendible Markdown compiler and some extensions add methods to its default export. Because of this bug I can't add proper typings to DefinitelyTyped and using the extension with TypeScript is a real pain.

    // @types/markdown-it
    declare module 'markdown-it'
    {
      interface MarkdownItStatic
      {
        new (): MarkdownIt;
        (): MarkdownIt;
        render(s: string): string;
      }
    
      let MarkdownIt: MarkdownItStatic;
    
      namespace MarkdownIt {
        export interface State { /* ... */ }
        /* ... */
      }
      export default MarkdownIt;
    }
    // @types/markdown-it-incremental-dom
    import * as MD from 'markdown-it';
    declare module 'markdown-it'
    {
      // Idea #1 (failed)
      interface MarkdownItStatic
      {
        renderToIncrementalDOM(): void;
      }
      let default: MarkdownItStatic;
      
      // Idea #2 (failed)
      declare default.renderToIncrementalDOM(): void;
      
      // Idea #3 (failed)
      interface MarkdownItStatic
      {
        renderToIncrementalDOM(): void;
      }
      
      // Idea #4 (failed)
      const default:
      {
        renderToIncrementalDOM(): void
      }
    }

    EDIT: I just realized that markdown-it is yet another type of evil – callable export = functionAndNamespace;. So the comment should read “consider a hypothetical reasonably typed module called markdown-it…”

  16. mattmccutchen commented on Oct 7, 2018

    @mattmccutchen
    Contributor

    m93a I don't think your scenario has anything to do with this issue since you aren't using a class. Your idea 3 works if I export the MarkdownItStatic interface in the first file. It isn't exported by default since the module has a default export expression (see this post), and it has to be exported in order to be augmented.

  17. rdsedmundo commented on Aug 15, 2019

    @rdsedmundo

    Is there a way for augmenting a module exported with export = though? I wasn't able to get it working.

    i.e

    knex.d.ts

    interface Knex {
      test1: string;
    }
    
    export = Knex;

    file.ts

    declare module 'knex' {
      interface Knex {
        test2: string;
      }
    }
  18. hanzhangyu commented on Oct 12, 2019

    @hanzhangyu
    Contributor

    default is a keyword of JS. By the way, you can export a new variable temporarily.

    node_modules -> knex.ts

    interface Knex {
      test1: string;
    }
    
    export = Knex;

    custom knex.ts

    import $knex from "knex";
    
    const knex = $knex as typeof $knex & {
      test2: string;
    }
    
    export default knex;
  19. danmana commented on May 6, 2020

    @danmana

    I just figured out a workaround for this while trying to augment chart.js, which is declared as a Class and namespace, and exported with export =

    node_modules @types/chart.js/index.d.ts

    declare class Chart { ... }
    declare namespace Chart { ... }
    export = Chart;
    export as namespace Chart;

    my-typings.d.ts

    import * as Chart from 'chart.js';
    
    declare global {
      interface Chart {
          panZoom: (increment: number) => void;
      }
    }

    usage in my-file.ts

    import * as Chart from 'chart.js';
    
    const chart = new Chart({...});
    chart.panZoom(5);

    I don't know exactly why, but it works even if in my file I import as a different name import * as ChartX from 'chart.js', so it will handle any refactoring/renames you make.

    I haven't tested if this also works with default exports.

    Here is the PR that I made for the chart.js augmentation DefinitelyTyped/DefinitelyTyped#44519

  20. clshortfuse commented on Jan 14, 2021

    @clshortfuse

    Was going a bit insane trying to figure our what was happening. Here's a small reproducible:

    /** @typedef {import('stream').Readable} */
    /** @typedef {import('stream').Writable} Writable */
    
    export default class MyClassName {
    
    }

    Merged declaration 'MyClassName' cannot include a default export declaration. Consider adding a separate 'export default MyClassName' declaration instead.ts(2652)

    That's because /** @typedef {import('stream').Readable} */ is incomplete. Once I change it to /** @typedef {import('stream').Readable} Readable */ the error goes away. This is the reproducible with JS. I'm not sure there's a Typescript analogy.

  21. neuoy commented on Feb 25, 2022

    @neuoy

    It seems to be working with the following convoluted setup:

    some-library-you-cant-modify/Foo.ts

    // This is the original class. You can't augment it directly because it's a default export.
    export default class Foo {
      existingProperty: number;
    }

    my-library/typings/Foo.ts

    import Foo from 'some-library-you-cant-modify/Foo'
    
    // just reexporting the same class, but this time it's not a default export
    export { Foo };

    my-library/typings/Foo.d.ts

    import { Foo } from './Foo';
    
    declare module './Foo' {
      interface Foo {
        myProperty: string; // class Foo is now augmented with this added property
      }
    }

    my-library/consumer.ts

    // Notice I'm importing from the original library, it works fine anyway.
    // This means you don't have to update any existing code.
    import Foo from 'some-library-you-cant-modify/Foo';
    
    let foo = new Foo();
    foo.existingProperty = 42; // we obviously can still access original properties
    foo.myProperty = ""; // the added property works fine too, no compilation error

    You'll have to add your Foo.d.ts file in my-library/tsconfig.json:

    {
      "files": [
        "typings/Foo.d.ts"
      ]
    }
  22. chmelevskij commented on Apr 20, 2022

    @chmelevskij

    Youen (@neuoy) solution kind of worked, but for me it broke other things 😓 Like building with rollup plugin typescript

  23. zhangone233 commented on Jul 31, 2023

    @zhangone233

    When will this issue be fixed ?

  24. GulgDev commented on Mar 31, 2025

    @GulgDev

    Are there any better workarounds than what Youen (@neuoy) suggested? I have a more complex case in which that would be harder to do.

  25. RyanCavanaugh commented on Aug 18, 2026

    @RyanCavanaugh
    Member

    The correct way to do this is

    import Foo from "./Foo";
    
    declare module "./Foo" {
      export default interface Foo {
        add(): void;
      }
    }
    
    new Foo().add();

    Rejecting the augmentation in OP is the correct behavior; export default class Foo does not make an export named Foo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugA bug in TypeScriptDomain: classesBehavior of various `class` constructs, e.g. mixins or base classes

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions