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

Ability to extend interfaces declared in ambient modules #280

Closed
fsoikin opened this issue Jul 28, 2014 · 5 comments
Closed

Ability to extend interfaces declared in ambient modules #280

fsoikin opened this issue Jul 28, 2014 · 5 comments
Labels
Fixed A PR has been merged for this issue Suggestion An idea for TypeScript

Comments

@fsoikin
Copy link

fsoikin commented Jul 28, 2014

Say I have an awesomely cool library:

export function createMyCoolObject( someArg ): MyCoolObject;

export interface MyCoolObject {
    func1(): void;
    func2( x: number ): string;
}

And it's so cool that I want people to be able to extend it.
So I add an extension point:

export function createMyCoolObject( someArg ): MyCoolObject;

// Extension point:
export var fn: MyCoolFunctions;

export interface MyCoolObject extends MyCoolFunctions {}

export interface MyCoolFunctions {
    func1(): void;
    func2( x: number ): string;
}

And now anybody can extend it like this:

import lib = module("MyCoolLib");

lib.fn.extFunc = function( whatever ) { }

Except this does not extend the type information. I cannot just do something like:

export interface lib.MyCoolFunctions {
    extFunc( whatever ) { }
}

(trust me, I've tried; you may start laughing now :-)

So instead, in order to make my library extensible, I have to split it into declaration (in the form of .d.ts) and implementation:

// MyCoolLib.d.ts:
module MyCoolLib {
    export interface MyCoolFunctions {
        func1(): void;
        func2( x: number ): string;
    }
}


// MyCoolLib.ts:
/// <reference path="MyCoolLib.d.ts" />
export function createMyCoolObject( someArg ): MyCoolObject;
export var fn: MyCoolFunctions;
export interface MyCoolObject extends MyCoolLib.MyCoolFunctions {}

And then everybody who wants to extend my library has to do the same.
Just like the good old C/C++ days! Makes you feel young again, doesn't it? :-))

But that's not the end of the PITA.
In addition to all the extenders having to split their extensions, all the users also now have two ways for referencing my library - one for type info, the other for implementation. Check this out:

import lib = module("MyCoolLib");

var x: MyCoolLib.SomeType = lib.createCoolObject( 1 ).someFunc();

See how I have two prefixes here? One is "MyCoolLib", the other - "lib".
But it gets weirder. Turns out interfaces can be in the .d.ts file, but classes must be in the .ts file, because they're implementation. So now I have to sometimes write "var x = MyCoolLib.SomeType" and other times write "var x = lib.SomeType", depending on what "SomeType" is.

In real applications, this gets complicated very quickly.
Check out this [almost] real piece of code using Rx:

class C {
   _d: Rx.IDisposable;

   constructor( e: X ) {
       this._d = rx.Disposable.create( () => e.destroy() );
   }
}

So the bottom line here is, we have to have an ability to extend interfaces coming from another module. At the very least something like this:

import lib = module("MyCoolLib");

interface lib.MyCoolFunctions {
   extFunc: ( whatever ) => void;
}

lib.fn.extFunc = (whatever) => { };

Yes, I do have to write the name and the signature of the function twice, but at least it works.
Ideally, though, I'd like to be able to write it once. Sort of like I do with C# extension methods.

@RyanCavanaugh
Copy link
Member

I think the new compiler allows this in a good way, because it allows merging of external modules (including between ambient and non-ambient external modules). Let me outline an example and see what you think

myLib.ts

export module Foo {
    export function fn1() { }
}

myExtension.ts

declare module "myLib" {
    export module Foo {
        export function fn2(): void;
    }
}

consumer.ts

/// <reference path="myExtension.ts" />
import lib = require('myLib');
lib.Foo.fn2(); // No error

Does this meet your scenario?

@fsoikin
Copy link
Author

fsoikin commented Jul 29, 2014

Yes, this totally works.

@RyanCavanaugh
Copy link
Member

Feel free to re-open if you run into a scenario this doesn't address. Thanks!

@nicojs
Copy link

nicojs commented Sep 10, 2016

This doesn't seem to work with the new workflow using @types imports.

For example:

npm i @types/estree;

// myEsTreeExtension.ts
declare module 'estree' {
    interface MyAddon {
   }
}

// Some other file.ts
import * as estree from 'estree';

let a: estree.Node; // ERROR: [ts] Module ''estree'' has no exported member 'Node'.

Is this a new issue? Or already known?

@mhegazy
Copy link
Contributor

mhegazy commented Jan 13, 2018

The file with the ‘declare module “..”’ needs to be a module as well. I.e. it needs a top level import or export.

@microsoft microsoft locked and limited conversation to collaborators Jun 25, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Fixed A PR has been merged for this issue Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

4 participants