Description
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?
- It provides a more clear way to write two very similar (but not identical) interface.
- Developers can know what different between this interface and what it's liking. (Oh, only property
name
is incompatible withPerson
, I can treat it as a little different type ofPerson
) - If a change was made in the original type, it will automatically appear in the liking interface.
Why not to introduce this?
- This proposal introduced a new keyword
likes
- This CFG is some bit of ambiguous about if
likes
is a variable name or a keyword. - Maybe this scenario is not common enough to introduce a new feature to it.
When to use it
- Only a few properties are different from another one, and they have no logically inherited relationship.
- If you cannot modify the parent interface (like
HTMLCanvasElement
) but you really want to "extends" from it.
When not to use it
- Only a few properties are needed from another interface. ( Just use
ISth['name']
) - You do not want to let other new properties automatically appear. ( Like you do not need new attributes on
HTMLElement
also appears on yourFakeElement
, you need a copy-paste ) - You can modify both
A
andB
and they have logically inherited(or whatever) relationship. ( You should find something common, make it intoC
, and letA
andB
extends fromC
) - The new interface is compatible with the original type. ( Just use
extends
)
Way to generate new type
Need to be precise
- After we finished dealing with extends, we get interface _extended
- 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 - Read all types in ObjectType, merge it into extended_liked
- We now get the final interface
Deal with confliction
interface _ likes A, B, ... extends C, D, ... { ...E }
- 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 )
- 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 typeA.X | B.X
- 3.a or 3.b, which one is better?
- 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 )
- 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.