Open vilicvane opened 7 years ago
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.
A workaround I follow when need extend from 2 or more interface is this:
type InputELement = InputHTMLAttributes<HTMLInputElement>;
type TextAreaElement = TextareaHTMLAttributes<HTMLTextAreaElement>;
type MyProps = InputELement & TextAreaElement & {
type: string;
name: string;
};
I'm curious what I'm missing. I'm pretty new to TS, but according to https://www.typescriptlang.org/docs/handbook/2/objects.html, it sure seems like you can extend multiple interfaces:
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
interface ColorfulCircle extends Colorful, Circle {}
const cc: ColorfulCircle = {
color: "red",
radius: 42,
};
If that's the case, shouldn't this issue be closed?
@zeckdude Yes you can extend two non-overlapping interfaces but this issue is concerned with two interfaces which have common identifier with different types. Have a look at the code provided in the issue.
Any updates on this?
I'm curious what I'm missing. I'm pretty new to TS, but according to https://www.typescriptlang.org/docs/handbook/2/objects.html, it sure seems like you can extend multiple interfaces:
interface Colorful { color: string; } interface Circle { radius: number; } interface ColorfulCircle extends Colorful, Circle {} const cc: ColorfulCircle = { color: "red", radius: 42, };
If that's the case, shouldn't this issue be closed?
@zeckdude it doesn't work if I use classes..
class A {
propA:string
}
class B {
propB: number
}
interface C extends A, B {
// ...
}
const foor: C = {}
it show multiple errors like:
TS2320: Interface 'C' cannot simultaneously extend types 'A' and 'B'. Named property 'propA' of types 'A' and 'B' are not identical.
looks like this also happens when an interface is augmented with the same method twice, even if the types are exactly the same:
interface Foo<T> {
a(): T
}
interface Foo<T> {
a(): T
}
interface Bar<T> {
a(): T
}
//Interface 'Baz<T>' cannot simultaneously extend types 'Foo<T>' and 'Bar<T>'.
// Named property 'a' of types 'Foo<T>' and 'Bar<T>' are not identical.(2320)
export interface Baz<T> extends Foo<T> , Bar<T> {}
This is definitely a noticeable omission to me when doing "diamond" interface inheritance.
Here's the simplest/best example I can come up with, based on what I'm implementing right now. Imagine you have an interface Foo and a subtype Bar that specializes some return types and adds some Bar-specific methods. Then you create ListenableFoo extends Foo, and you want to combine ListenableFoo and Bar to make ListenableBar:
interface Foo<T> {
copy(): Foo<T>
}
interface Bar extends Foo<number> {
copy(): Bar; // specialized return value
someBarOnlyMethod(): void;
}
interface ListenableFoo<T> extends Foo<T> {
listen(): void;
}
interface ListenableBar extends ListenableFoo<number>, Bar {
copy(): Bar // need to repeat the specialized method or else error
}
In an ideal world, I would not have to repeat the signature of copy()
in ListenableBar
. TypeScript would see that Bar's type for the copy
method is not identical to ListenableFoo<number>
's, but it is compatible.
An alternative workaround is to write:
interface ListenableBar extends ListenableFoo<number>, Bar {
copy: Bar["copy"]
}
In this example, I was expecting
SomeChange
to have a type equivalent to:But it would result in error:
In this case,
'some'
is compatible withstring
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.