Suppose I have two classes, a Dog
and an Animal
class. In this example, Dog extends Animal
.
Now suppose I have two additional classes (one called Parent
, and another called Child
). In TypeScript, I am able to type the arguments of my child class's constructor function to be that of my parent class's constructor function's arguments using ConstructorParameters
like so:
class Parent {
constructor(a: Animal, b: number, c: number) {}
}
class Child extends Parent {
constructor(...args: ConstructorParameters<typeof Parent>) { // same as parent's arguments: a: Animal, b: number, c: number
super(...args); // doesn't complain, as args matches the arguments for Parent ([a: Animal, b: number, c: number])
}
}
In this example, new Child()
and new Parent()
would accept the same argument types. But how could I type the Child constructor function to accept a stricter type for a
, such as Dog
?
class Parent {
constructor(a: Animal, b: number, c: number) {}
}
class Child extends Parent {
// v----- want to access a in `Child`'s constructor
constructor(a: Dog, ...args: ConstructorParameters<typeof Parent>) {
super(a, ...args); // <-- complains, I want to pass `a: Dog` and ...args (containing [b: number, c: number])
a.bark(); // use specific Dog methods (that don't exist on Animal)
}
}
// Using:
// - new Child(new Animal(), 0, 0); should complain as Animal is not of type Dog
// - new Child(new Dog(), 0, 0); should work as Dog is of type Dog
The above doesn't work, as Child now expects to be passed a Dog
instance followed by the three arguments for the Parent
constructor. I was thinking of using Omit<>
or Partial<>
on ConstructorParameters
to remove the expected a: Animal
type but couldn't seem to get those to work. I'm also looking for a solution that doesn't involve me needing to pass through the arguments individually to the super()
call (ie: if I add arguments in Parent's constructor, I won't need to add them to the Child).
CodePudding user response:
try conditional types
type RestWithoutOne<K> = K extends [infer WithThis, ...infer WithRest] ? WithRest : never;
class Child extends Parent {
// v----- want to access a in `Child`'s constructor
constructor(a: Dog, ...args: RestWithoutOne<ConstructorParameters<typeof Parent>>) {
const a = args[0];
super(a, ...args); // <-- complains, I want to pass `a: Dog` and ...args (containing [b: number, c: number])
a.bark(); // use specific Dog methods (that don't exist on Animal)
}
}
CodePudding user response:
But how could I type the
Child
constructor function to accept a stricter type fora
, such asDog
?
That sounds like a bad idea, violating the Liskov substitution principle: one should be able to use Child
everywhere where Parent
can be used.
I would therefore recommend generics:
class Parent<T extends Animal> {
constructor(a: T, b: number, c: number) {}
}
class Child extends Parent<Dog> {
constructor(...args: ConstructorParameters<typeof Parent<Dog>>) {
super(...args);
const a = args[0];
a.bark(); // use specific Dog methods (that don't exist on Animal)
}
}