Skip to content

Proposal: Introduce a new way to define "inherited" interfaces: likes #18762

Closed
@Jack-Works

Description

@Jack-Works

If anyone has interest in this proposal, please let me know, I may do more future work (like try to implement it)

Sometimes, we need a way to introduce an interface that is incompatible with original type but very likes to the original type.

For example, in node-canvas package, they provide a class Canvas that very likes to the HTMLCanvasElement, but it is not compatible with HTMLCanvasElement. If we can have a clear way to "clone" an interface and make some changes, we can just use this way instead of copy-paste everything in the original type.

Proposal: Introduce a new way to define "inherited" interfaces: likes

interface A { name: string, id: number }

interface B likes A { id: typeof uuid }

interface C likes B, A extends X {}

Changes to the grammar

Change InterfaceDeclaration to:

interface ‎BindingIdentifier ‎TypeParameters(opt) ‎InterfaceLikesClause(opt) ‎InterfaceExtendsClause (opt) ‎ObjectType

Add InterfaceLikesClause:
likes ClassOrInterfaceTypeList

Why to introduce this?

  1. It provides a more clear way to write two very similar (but not identical) interface.
  2. ‎Developers can know what different between this interface and what it's liking. (Oh, only property name is incompatible with Person, I can treat it as a little different type of Person)
  3. ‎If a change was made in the original type, it will automatically appear in the liking interface.

Why not to introduce this?

  1. This proposal introduced a new keyword likes
  2. ‎This CFG is some bit of ambiguous about if likes is a variable name or a keyword.
  3. ‎Maybe this scenario is not common enough to introduce a new feature to it.

When to use it

  1. Only a few properties are different from another one, and they have no logically inherited relationship.
  2. ‎If you cannot modify the parent interface (like HTMLCanvasElement) but you really want to "extends" from it.

When not to use it

  1. Only a few properties are needed from another interface. ( Just use ISth['name'] )
  2. You do not want to let other new properties automatically appear. ( Like you do not need new attributes on HTMLElement also appears on your FakeElement, you need a copy-paste )
  3. You can modify both A and B and they have logically inherited(or whatever) relationship. ( You should find something common, make it into C, and let A and B extends from C )
  4. The new interface is compatible with the original type. ( Just use extends )

Way to generate new type

Need to be precise

  1. After we finished dealing with extends, we get interface _extended
  2. ‎Do all the same things just like extends expect one thing: Inherited properties with the same name must be identical (section 3.11.2). (In typescript spec, 7.1), that means this is a conflict friendly version of interface extends.
    Now we get interface extended_liked
  3. Read all types in ObjectType, merge it into extended_liked
  4. ‎We now get the final interface

Deal with confliction

interface _ likes A, B, ... extends C, D, ... { ...E }
  1. If anything with the same name is not compatible in C and D, throw an Error ( Property X in C and D are not compatible )
  2. ‎If anything with the same name is not compatible in A and C (or B and D, and so on), use declaration in C (or D)
    3.a ‎If anything with the same name is not compatible in A and B, check if it is defined in E, if not, throw an Error ( Property X in C and D is not compatible and also not defined in the interface body, cannot determine which one to use )
    3.b If anything with the same name is not compatible in A and B, make this property as type A.X | B.X
  3. 3.a or 3.b, which one is better?
  4. ‎If anything with the same name is not compatible in E and C, throw an Error ( Property X in the interface body are not compatible with X defined in C )
  5. ‎If anything with the same name is not compatible with E and A, use that in E

For example:

interface Foreigner { name: string, id: number }
interface Student { id: typeof uuid }
interface ForeignerStudent likes Foreigner extends Student {  }

// ForeignerStudent is a subtype of Student, but not a sub-type of Foreigner
// ForeignerStudent = { name: string, id: typeof uuid }

3.a

interface Foreigner { name: string, id: number }
interface Student { id: typeof uuid }
interface ForeignerStudent likes Foreigner, Student {  }
// Error: Property id is not compatible
//     typeof uuid is not compatible with number
//          You need to specify a type for id
interface ForeignerStudent2 likes Foreigner, Student { id: typeof uuid }
// This is fine.

3.b

interface Foreigner { name: string, id: number }
interface Student { id: typeof uuid }
interface ForeignerStudent likes Foreigner, Student {  }
// ForeignerStudent = { name: string, id: number | typeof uuid }

Others

What if I want to remove a property inherited by likes?
I have 2 ideas at present

interface N likes M {
    y: never // this way
    ‎delete x // or this way
}

If anyone has interest in this proposal, I will consider it later.

When will a liked interface compatible with the original interface?
Just treat it as an another identical normal interface.

Does it breaks the type system?
I don't think so. It is not a subtype of the original type.

What about generics work with proposal?
No idea, I will think it later.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions