interface Change {
uid: string;
type: string;
}
interface SomeChangeExtension {
type: 'some';
foo: number;
}
interface SomeChange extends Change, SomeChangeExtension { }
In this example, I was expecting SomeChange
to have a type equivalent to:
interface SomeChange {
uid: string;
type: 'some';
foo: number;
}
But it would result in error:
Interface 'SomeChange' cannot simultaneously extend types 'Change' and 'SomeChangeExtension'.
Named property 'type' of types 'Change' and 'SomeChangeExtension' are not identical.
In this case, 'some'
is compatible with string
if the order of interfaces being extended is respected.
The reason why I want this to be allowed is that, I need to maintain multiple interfaces of the same kind of change during different stages: raw change, change, broadcast change. And having to duplicate part of the "extension" on every of them doesn't look good.
So what is the proposal here? use intersection types?
@mhegazy I was thinking about respect the extensions order, and if the later one is compatible with the former one, then use the later one instead.
E.g.:
interface You {
name: string;
value: number;
}
interface FooYou {
name: 'foo';
}
interface ConcreteFooYou extends You, FooYou {
concrete: boolean;
}
In which ConcreteFooYou
should be equivalent to:
interface ConcreteFooYou {
name: 'foo';
value: number;
concrete: boolean;
}
Having the ability to perform multiple extends
is something that I've personally been waiting for for a very long time from TS, it would really bring a lot to the language!
At some point I tried hacking about with the following to try to add this behaviour (I can't remember exactly what, and I know that the below doesn't work), but I haven't been able to make anything sit correctly with regards to types:
// Using two traits for the sake of an example, but obviously this would be overloaded
function mix<T1, T2>(trait1: T1, trait2: T2): new(...args: any[]) => T1 & T2 {
// ... merge the prototypes of the two parent classes
}
In terms of runtime functionality, there was no issue what-so-ever with the merging of prototypes. Regarding conflicts in method or property names, the conflicting parent class takes a backseat thereby respecting the usual order of inheritance, the only real issue is the lack of the correct super
behaviour (and of course the fact that this is really just a bad hack).
Having the ability to perform multiple extends is something that I've personally been waiting for for a very long time from TS, it would really bring a lot to the language!
you can extend multiple interfaces. for classes, you can do this using mixins.
Mixins require you to redeclare the types in the implementing class, which is pretty messy in large projects. What the community would benefit more from is a similar behaviour to Scala traits.
That is a discussion i would suggest bringing to TC39. any thing we do in this space can not conflict with future JS direction.
I just ran into this issue, here's my use case:
I was trying to use the public API I define in xterm.d.ts inside the actual library, instead of just reimplementing it. But the public API contains a specific overloads for Terminal than the actual Terminal class, for example:
on(type: 'blur' | 'focus' | 'linefeed' | 'selection', listener: () => void): void;
Since I was using multiple inheritance they were conflicting:
export interface ITerminal extends PublicTerminal, IEventEmitter, ... { ... }
This conflicts with the generic IEventEmitter
interface:
on(type: string, listener: (...args: any[]) => void): void
Here's a small snippet that demonstrates my specific problem:
interface IBase1 {
f(arg: 'data'): void;
f(arg: string): void;
}
interface IBase2 {
f(arg: string): void;
}
interface IOther extends IBase1, IBase2 {
}
Interface 'IOther' cannot simultaneously extend types 'IBase1' and 'IBase2'.
Named property 'f' of types 'IBase1' and 'IBase2' are not identical.
I think in the end I can work around this by moving IEventEmitter
into the .d.ts but ideally I didn't really want to expose all those methods (doesn't matter too much though). It now looks something more like this:
interface IBase1 extends IBase2 {
f(arg: 'data'): void;
f(arg: string): void;
}
interface IBase2 {
f(arg: string): void;
}
interface IOther extends IBase1 {
}
Bumping this. Would be nice to be able to...
import { IControlPanelRoutes } from "control-panel"
import { IHomepageRoutes } from "home-page";
interaface IAllRoutes extends IControlPanelRoutes & IHomepageRoutes {};
props.routing: IAllRoutes = {
controlPanel: "/control-panel",
controlPanel_overview: "/control-panel/overview",
controlPanel_settings: "/control-panel/settings",
homePage: "",
homePage_about: "/about",
}
I think you already can:
interface IAllRoutes extends IControlPanelRoutes, IHomepageRoutes {};
Just a catch, if the objects share same property names, the types need to match.
has any ground been made on multiple extends?
developers need object oriented abilities for real world modeling
in java this is permitted in interfaces
this should be ok in typescript at least for interfaces
declaration merging seems to be what Im looking for
https://www.typescriptlang.org/docs/handbook/declaration-merging.html
@NewEraCracker You missed the whole point, the request here is to allow compatible (rather than identical) types to match.
+1 to compatible types when extending multiple interfaces.
If we define SomeChange
with type alias and intersection we end up with the expected type.
type SomeChange = Change & SomeChangeExtension;
// end up typed as { uid: string; type: 'some'; foo: number; }
Anyway this is not a solution because we lose the properties of the ts interface
.
Why the type intersection is different from the interface extension?
Different syntax does different stuff. It's how we let you write different types 馃槈
+1
I'm try to model Ldap ObjectClass like array type in place and I'm facing issue that I can't combine two interfaces for object as they don't share same enum values. (even if any enum value is in allowed to objectClass in main level interface)
https://stackoverflow.com/questions/54019627/building-combined-interface-array
If we define
SomeChange
with type alias and intersection we end up with the expected type.type SomeChange = Change & SomeChangeExtension; // end up typed as { uid: string; type: 'some'; foo: number; }
Anyway this is not a solution because we lose the properties of the ts
interface
.
Why the type intersection is different from the interface extension?
@manugb
Why can't you use your type, then make an interface that extends SomeChange
?
Or does this not get you what you're after?
interface SomeMoreChange extends SomeChange {
// props of Change and SomeChangeExtension...
// additional props...
}
@vilic This should help you
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
interface Change {
uid: string;
type: string;
}
interface SomeChangeExtension {
type: 'some';
foo: number;
amount: number;
}
interface SomeChange extends Change, Omit<SomeChangeExtension, 'foo' | 'amount'> {
foo: string;
amount: string;
}
interface Message { kind: string }
interface Request extends Message { payload: string }
interface Response extends Message { success: boolean }
interface HelloMessage extends Message { kind: 'hello' }
interface HelloRequest extends HelloMessage, Omit<Request, 'kind'> { ... }
interface HelloResponse extends HelloMessage, Omit<Response, 'kind'> { ... }
interface GoodbeMessage extends Message { kind: 'goodbye' }
type GoodbyeRequest = GoodbyeMessage & Request & { ... }
type GoodbyeResponse = GoodbyeMessage & Response & { ... }
The above shows the two ways I have figured out how to make this work, I believe both come with their own caveats. It certainly _feels_ like extending from two conflicting interfaces where one is a narrowing of the other should "just work". My expectation, like others here, is that TypeScript should treat it like an intersection, just like the type solution above does. In this particular case, the kind
comes from Message
in both base types (Request
and HelloMessage
), its just that in one type path has narrowed kind
while the other has not, so we can be guaranteed (at least in this situation) that the types are compatible with narrowing.
Another option I just discovered:
interface AppleMessage extends Message { kind: 'apple' }
interface AppleRequest extends AppleMessage, Request { kind: 'apple' }
interface AppleResponse extends AppleMessage, Response { kind: 'apple' }
While this one isn't DRY, you'll get a compiler error if you put anything other than kind: 'apple'
in the AppleRequest
and AppleResponse
, so you can't really screw it up.
Following gives compilation error as well
```
interface IParent
{
somData: Object;
}
interface IChild extends IParent
{
someData: Object;
}
interface IHavePropertyOfTypeParent
{
fields: IParent[];
}
interface IHavePropertyOfTypeChild
{
fields: IChild[];
}
interface IMustBeInferredToHavePropertyOfTypeParent extends IHavePropertyOfTypeParent, IHavePropertyOfTypeChild
{
}
Workaround to make it work is
```
interface IMustBeInferredToHavePropertyOfTypeParent extends IHavePropertyOfTypeParent, IHavePropertyOfTypeChild
{
fields: IParent[];
}
I have another example where being able to extend multiple interfaces with compatible types would be very useful.
Consider the EventEmitter class, which is very useful to extend in Node when creating custom classes. It has many functions with similar signatures, taking a string (the event name) and a function (the event handler).
It would be beneficial to redefine those functions with more specific types for the event names and the event handlers, but doing so is very verbose and tedious.
See this Typescript Playground example to see what I mean: Playground
Another quite unfortunate example of this issue is as follows:
interface Obj {
"f": () => string | number;
}
// Works due to covariance: narrower return type for method is allowed
interface ObjSub extends Obj {
"f": () => string;
}
// This effectively extends both ObjSub and Obj, and works
interface ObjSubSub extends ObjSub {}
// But when Obj is added explicitly, these interfaces are deemed incompatible
interface ObjSubSub2 extends ObjSub, Obj {}
It shows a case where two interfaces are deemed compatible when one extends the other, but when another interface explicitly extends from both they are considered incompatible. That sounds inconsistent, or is there something I'm overlooking here?
If you need this feature like me for event emitter you could use the combined variation suggested above like so:
// this is the class you want to attach events typings
class Example {
// implement / extend the logic to actually have the addEventListener etc.
}
// if we would like to attach 4 possible events with the event type Structure
EventEmitDef<'exampleA', Structure>
EventEmitDef<'exampleB', Structure>
EventEmitDef<'exampleC', Structure>
EventEmitDef<'exampleD', Structure>
// You would define the interface for those event by their specific methods to help the method inference
interface EventEmitDef<Name extends string, Structure extends object> {
addEventListener(name: Name, listener: (ev: Structure) => void): void;
dispatch(name: Name, event: Structure): void;
removeListener(name: Name, listener: (ev: Structure) => void): void;
}
// which you would apply on the class by mixins
interface Example extends
EventEmitDef<'exampleA', Structure>,
EventEmitDef<'exampleB', Structure>,
EventEmitDef<'exampleC', Structure>,
EventEmitDef<'exampleD', Structure> {}
// Which does not work currently, but instead you could join them first and then extend them
type SimpleEventGroup =
EventEmitDef<'exampleA', Structure> &
EventEmitDef<'exampleB', Structure> &
EventEmitDef<'exampleC', Structure> &
EventEmitDef<'exampleD', Structure>
interface Example extends SimpleEventGroup {}
The mixing by extending the joined event group works out and TypeScript can correctly infer the methods present on the class. So addEventListener
, removeEventListener
and dispatch
only allow the correct event names and the correct event structure types.
The above mixin translates to:
interface ActualExample{
addEventListener(name: 'exampleA', listener: (ev: Structure) => void): void;
addEventListener(name: 'exampleB', listener: (ev: Structure) => void): void;
addEventListener(name: 'exampleC', listener: (ev: Structure) => void): void;
addEventListener(name: 'exampleD', listener: (ev: Structure) => void): void;
dispatch(name: 'exampleA', event: Structure): void;
dispatch(name: 'exampleB', event: Structure): void;
dispatch(name: 'exampleC', event: Structure): void;
dispatch(name: 'exampleD', event: Structure): void;
removeListener(name: 'exampleA', listener: (ev: Structure) => void): void;
removeListener(name: 'exampleB', listener: (ev: Structure) => void): void;
removeListener(name: 'exampleC', listener: (ev: Structure) => void): void;
removeListener(name: 'exampleD', listener: (ev: Structure) => void): void;
}
If now you could change the class type by a decorator, it would be perfect 馃榿
EDIT: Sadly this seems to destroy the suggestions provided by the language server, which means that you still receive compile errors as intended but are missing the live suggestions of the strings possible.
Most helpful comment
@mhegazy I was thinking about respect the extensions order, and if the later one is compatible with the former one, then use the later one instead.
E.g.:
In which
ConcreteFooYou
should be equivalent to: