I defined an applyDamage method to accept number or string, but implemented it with only with a number argument. When I call this method using base class, I have incorrect behavior. Why doesn't TypeScript show an error?
interface Character {
applyDamage(value : number): number;
}
interface Humanoid extends Character {
hp:number;
applyDamage(value: number|string): number
}
class Monster implements Humanoid {
hp:number = 10;
applyDamage(v: number) {
this.hp -= v;
return v;
}
}
const monster:Humanoid = new Monster();
monster.applyDamage("hello");
console.log(monster.hp); // <-- NaN
CodePudding user response:
Unfortunately this is one of the loopholes in type safety caused by the fact that method parameters are checked bi-variantly. This means that as long as there is a relationship between the function parameters it doesn't matter in which direction that relationship is.
Here's a link to the related TypeScript handbook section: Function Parameter Bivariance
The reasoning for this is explained in the PR that introduces strict function types (ie contravariant parameter types for function signatures) and is basically that if methods were checked contravariantly it would result in most generic types being invarinat (so you couldn't assign Array<Cat> to Array<Animal>
One solution is to avoid method signatures and use function signatures wherever possible:
interface Character {
applyDamage: (value : number) => number;
}
interface Humanoid extends Character {
hp:number;
applyDamage: (value: number|string) => number
}
class Monster implements Humanoid {
hp:number = 10;
applyDamage(v: number) { // error
this.hp -= v;
return v;
}
}
const monster:Humanoid = new Monster(); // error
monster.applyDamage("hello");
console.log(monster.hp); // <-- NaN
If you want to learn more about variance, you can watch my presentation on it here
CodePudding user response:
As others have pointed out: an overload would work... but the way that I read your code, it seems like you only want to implement a subtype of the union parameter, so I think generics are the right answer to your issue. By also providing a default type parameter for the generics, they can be used more ergonomically.
interface Character<T extends string | number = number> {
applyDamage (value: T): number;
}
interface Humanoid<T extends string | number = string | number> extends Character<T> {
hp: number;
}
class Monster implements Humanoid<number> {
hp: number = 10;
applyDamage(v: number) {
this.hp -= v;
return v;
}
}
////////// Use:
const monster: Humanoid<number> = new Monster();
// You could also just write it this way:
// const monster = new Monster();
monster.applyDamage("hello"); /*
^^^^^^^
Argument of type 'string' is not assignable to parameter of type 'number'.(2345) */
console.log(monster.hp); //=> NaN
CodePudding user response:
You defined the signature of the method in Humanoid for applyDamage to take an input value of string | number;
TS just checks that your implementation for Monster class has the appropriate signature and returns the defined type. It's up to you to write a method body that is safe.
It seems that maybe you want to use a overload. https://www.typescriptlang.org/docs/handbook/2/functions.html#function-overloads
