Skip to content

Can't extend the same interface with different type parameters #11700

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

Closed
pimterry opened this issue Oct 18, 2016 · 3 comments
Closed

Can't extend the same interface with different type parameters #11700

pimterry opened this issue Oct 18, 2016 · 3 comments
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@pimterry
Copy link
Contributor

pimterry commented Oct 18, 2016

This might sound like obviously correct behaviour. There's a very strong use case in the Node.js type definitions alone though, and I think type aliases show an avenue for this that could become a manageable workaround that's accessible without too much work.

Motivation

In the Node type definitions, we have a huge amount of duplication for EventEmitter types, which could be removed if we could write something like this:

interface EventEmitter<EventName extends string, CallbackType extends Function> {
        addListener(event: EventName, listener: CallbackType): this;
        on(event: EventName, listener: CallbackType): this;
        once(event: EventName, listener: CallbackType): this;
        prependListener(event: EventName, listener: CallbackType): this;
        prependOnceListener(event: EventName, listener: CallbackType): this;
        removeListener(event: EventName, listener: CallbackType): this;
}

interface Server extends EventEmitter<"connection", (s: Socket) => void>,
                         EventEmitter<"error", (e: Error) => void>,
                         EventEmitter<"listening", () => void>,
                         EventEmitter<"close", () => void> {
  // ...other non-event emitter methods
}

Currently in the Node type definitions we implement EventEmitters like the above by duplicating every one of the EventEmitter methods for every single event that an object can have. That results in a huge amount of code in lots of places. These interfaces don't even bother typing all the methods they could (e.g. removeListener, listenerCount, listeners) presumably because that would make this even worse.

This would all be drastically simplified and made much more maintainable if something like the above was possible.

Type alias approach

In a perfect world, it would be wonderful if this Just Worked, and the above code defined method overrides for each of the combinations where they conflicted, more or less equivalent to the currently manually maintained net.Server definition.

I've seen other explanations of why this won't work with interfaces directly though. Unfortunately the suggested workaround for this problem generally is to manually merge the method declarations, which would defeat the point here.

You can get closer with type aliases:

// You can define this just fine, unlike the interface equivalent, solving half the problem.
type ServerEmitter = EventEmitter<"connection", (s: Socket) => void> &
                     EventEmitter<"error", (e: Error) => void> &
                     EventEmitter<"listening", () => void> &
                     EventEmitter<"close", () => void>;

This isn't a fully equivalent alternative though; critically you can't extend this type with another interface, or extend from the intersection expression itself (TS2312: An interface may only extend a class or another interface / TS2499: An interface can only extend an identifier/qualified-name with optional type arguments).

Options

There's a few things TypeScript could do that would solve this:

  1. Make interface extension work as in the first example (sounds like this isn't practical)
  2. Allow extending type aliases in interfaces
  3. Allow extending type expressions (A & B) in interfaces

I haven't dug into this in depth, but either 2 or 3 sound like they would work around the issues with 1 (as the rules for merging are well defined), and would provide relatively clean solutions to solving these problems generally.

@mhegazy
Copy link
Contributor

mhegazy commented Oct 18, 2016

Extending a type alias is not the real issue here. The real issue is ordering signatures. Overload resolution in TS is order dependent. So the issue is how these overloads are merged. for instance:

declare function doWork(a: string) : number;
declare function doWork(a: "command" ) : string;

declare function doWork2(a: "command" ) : string;
declare function doWork2(a: string) : number;

doWork("command"); // returns number

doWork2("command"); // returns string;

In your EventEmitter example, you have something like addListener that needs to get an overload from each one, but which one comes first, should order of interfaces in extends clause matter? what if one of them is more generic than the previous ones, should it hide it? etc.. The error you get today is just an indication that the resulting interface needs to re-specify the order of overloads. obviously respesfiying the overloads defines the whole point of minimizing the duplication.

I do agree that it would be good to enable the scenario listed above. but for this to happen we first need #10523 addressed with an algorithm that allows for order-independent literal types resolution. then we need to identify when inheriting the same signature from multiple interfaces would be ok under that algorithm and no show an error when it happens.

@mhegazy mhegazy added Suggestion An idea for TypeScript Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Oct 18, 2016
@begincalendar
Copy link
Contributor

Not sure if this issue is related, but it seems like it is.

The Redux issue was resolved, but the API it leaves behind is not ideal (because of the inability to overload the same interface with different type parameters).

@RyanCavanaugh
Copy link
Member

I think existing solutions are sufficient here given how often the use case arises

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

4 participants