Description
Search Terms
Inherit, inheritance, type inference, subclass, superclass, override,
Suggestion
This issue addresses the lack of easy way to refer to types of fields or methods of a base class or implemented interfaces without a high amount of repetition. Proposed is a keyword inherit
which will act as a type alias which is contextually resolved for the corresponding type of the base class.
A simpler way to understand this proposal is to think of it as a solution to #10570 by offering an explicit but convenient way to refer to the base class, let us look at a typical example:
abstract class Geometry {
public abstract model: "square" | "circle";
}
class Canvas extends Geometry {
// initially set to square but still allow to change to circle later.
public model = "square";
// ERROR ^ Type 'string' is not assignable to type '"square" | "circle"'
}
In this case we want to preserve the same type for field model
from the class Geometry
. the most obvious solution is to rewrite "square" | "circle"
but we can do a little better by instead using Geometry["model"]
to allow for changes in the base class to propagate automatically.
class Canvas extends Geometry {
public model: Geometry["model"] = "square";
// This way we explicitly inherit the same type as base class.
}
This is better but still requires us to refer to the base class and rewrite the field name for each field initialized in this way. The proposed solution is to add a keyword inherit
which will use the base class and the field name to resolve to the appropriate type.
class Canvas extends Geometry {
public model: inherit = "square";
// This would be equivalent to above example using Geometry["model"]
}
Here is an example playground where I define inherit
as a type alias where the arguments could be gained automatically from the context of where it is used.
This would also support being used in method parameters and return types which would be equivalent to using Parameters
and ReturnType
where appropriate. For example each of these 3 declarations would be equivelent to the following comment
class WithInherit extends Base {
a: inherit;
// a: Base["a"]
b: Readonly<inherit>; // note that we can also compose it with other types
// b: Readonly<Base["b"]>
foo(param: inherit): inherit {}
// foo(param: Parameters<Base["foo"]>[0]): ReturnType<Base["foo"]>{}
}
This would also address #2000 with additional capability, if all parameters and return type use inherit
it will ensure the call signature exactly matches the super class. except without forcing redefining it and allowing extensions like Readonly<inherit>
shown above or "fly" | inherit
shown below.
Use Cases and Examples
Composition
The proposed keyword would not be required to use alone but could be used like any other type alias. This means that further additions to the original type can be done such as Partial<inherit>
or similar. Consider this example where we want to override a method doAction
to accept additional kinds of parameters:
class FlyingRobot extends Robot {
doAction(action: inherit | "fly"){
// type of input is either "fly" or something the base class supports
if(action == "fly"){
// support our case
return this.fly()
} else {
// any other value must be valid to base class since we used inherit.
return super.doAction(action);
}
}
fly(){}
}
This allows us to support an obvious variation of the type defined by the base class and can be easily understood and written without even looking at the original declaration of Robot[“doAction”]
.
Interfaces and Multiple Implementations
In the event that a class implements several interfaces, inherit
will look up fields on an intersection of the base class (if present) and all implemented interfaces:
class Foo extends Base implements A, B {
a: inherit; // resolves to (Base & A & B)["a"]
}
For cases where multiple interfaces would define the same method or field inherit
would resolve to the intersection of all necessary values, or in the case of method overrides resolve only to the last method to remain consistent with Parameters
and similar constructs.
Constraints, Valid and Invalid Uses.
The inherit
keyword only makes sense inside a class that either extends another class and/or implements at least one interface. If there is no base class then an error should be raised. As well the inherit
keyword would only be applicable in one of the following positions:
- Instance field
- Getter return value
- Setter parameter
- Method parameter
- Method rest parameter
- Method return value
For parameters and return types, if the corresponding field on the base class is not a function then a compile error should be thrown. Either with a very similar message to the one thrown by Parameters<0>
or one more specific like “cannot inherit parameter when super field “foo” is not a function”.
For rest parameters, inherit
should be equivalent to a extends / infer
statement with the same number of other parameters as specified, with all other types set to any
for the inference. So if the function is written like foo(a: number, b: string, …rest: inherit)
the inherit would resolve like this:
Base[“foo”] extends (a:any, b:any, ...rest: infer T)=>any ? T : never
. And the never
there would never be used since if Base[“foo”]
is not a function then a compile error is thrown. Any other case even where Base[“foo”]
takes only 1 parameter will still give valid results for the rest parameter.
Note that for fields initialized to arrow functions, it is not valid to use inherit
in the parameters or return type of the arrow function. Instead the full field can use inherit
then the parameters will be inferred using normal means: Playground Link
class Example implements Base {
// a and b here are automatically typed when `inherit` resolves to a valid function type. this is nothing new.
foo: inherit = (a, b) => {};
}
Cases that don’t work
Methods with several overloads and methods with generics are 2 cases where the currently existing Parameters
and similar construct cannot fully capture the information. This proposal would suggest that no additional work be done to let inherit
do any extra work in these cases but does recognize that that would be possible in the future. This means that if a method has a generic the inherit
keyword will only use the constraint without preserving the generic. Similarly when a method has several overloads inherit
will only use the type specified in the last overload which without specifying the other necessary call signatures is not valid.
Checklist
My suggestion meets these guidelines:
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
*
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
- This feature would agree with the rest of TypeScript's Design Goals.
*
in order to not break any existing code that may use a type alias called inherit
, this would need to allow the keyword to be shadowed similar to other built in names.