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

Can not declaration merging for default exported class #14080

Open
liujunxing opened this issue Feb 15, 2017 · 17 comments

Comments

@liujunxing
Copy link

commented Feb 15, 2017

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.

@liujunxing

This comment has been minimized.

Copy link
Author

commented Feb 15, 2017

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

This comment has been minimized.

Copy link

commented Feb 15, 2017

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.

@Zaibot

This comment has been minimized.

Copy link

commented Mar 10, 2017

Could this be related to an old bug? #2537

@mhegazy

This comment has been minimized.

Copy link

commented Mar 10, 2017

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

@Zaibot

This comment has been minimized.

Copy link

commented Mar 13, 2017

Sorry, misread or mixed up the thread

@jtlapp

This comment has been minimized.

Copy link

commented Sep 28, 2017

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

@jtlapp jtlapp unassigned yuit Sep 28, 2017
@jtlapp

This comment has been minimized.

Copy link

commented Sep 28, 2017

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.

@jtlapp

This comment has been minimized.

Copy link

commented Sep 28, 2017

P.S. How did I automatically unassign @yuit?

@jtlapp

This comment has been minimized.

Copy link

commented Sep 28, 2017

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?

@nevir

This comment has been minimized.

Copy link

commented Sep 29, 2017

@jtlapp

This comment has been minimized.

Copy link

commented Sep 29, 2017

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

@nevir

This comment has been minimized.

Copy link

commented Sep 29, 2017

@mattmccutchen

This comment has been minimized.

Copy link
Contributor

commented Sep 17, 2018

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.

@m93a

This comment has been minimized.

Copy link

commented Oct 7, 2018

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…”

@mattmccutchen

This comment has been minimized.

Copy link
Contributor

commented Oct 7, 2018

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

@rdsedmundo

This comment has been minimized.

Copy link

commented Aug 15, 2019

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

This comment has been minimized.

Copy link

commented Oct 12, 2019

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;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
10 participants
You can’t perform that action at this time.