Reputation: 754
I stumbled accross the following scenario recently:
class A {
public m1(x: string | string[]): string | string[] {
return this.m2(x);
}
protected m2(x: string | string[]): string | string[] {
return x;
}
}
class B extends A {
protected m2(x: string[]): string { // no compiler warning
return x.join(',');
}
}
const b = new B();
console.log(b.m1(['a', 'b', 'c'])); // ok
console.log(b.m1('d')); // runtime error
Is this a bug in the TypeScript typing system or intentional? If the latter, how can I change the typing so that the compiler will recognize the problem?
Upvotes: 3
Views: 231
Reputation: 3485
In TypeScript, methods are bivariant and function properties contravariant with --strictFunctionTypes
. Note their different syntax:
class MyClass {
public fn(a: string): string { ... } // this is a method
public fn = (a: string): string => { ... } // this is a function as property
}
To enable stricter typing for m2
, you need to have --strictFunctionTypes
enabled (default with strict
) and use a function property:
protected m2 = (x: string | string[]): string | string[] => {
return x;
}
Now, m2
will error properly and you will need to distinguish between string
and string[]
:
protected m2 = (x: string | string[]): string => {
return Array.isArray(x) ? x.join(',') : `${x},`;
}
Live code example on Playground
Related: What are covariance and contravariance?
Related: JS class fields
Upvotes: 1
Reputation: 565
This is expected an behaviour. In typescript, you override a method to funnel down the specificity of the method instead of widening it as in other languages.
As for the explanation...
class A {
public m1(x: string | string[]): string | string[] {
/**
* Here variable 'x' is of type string | string[], which perfectly overlaps
* with the arguments for the method m2. So you won't get a compile-time error
*/
return this.m2(x);
}
protected m2(x: string | string[]): string | string[] {
return x;
}
}
class B extends A {
/**
* Here method m2 is overridden by a method which accepts a string[], which
* is covered by the parent's method, and hence no compile-time error. If
* you made the argument type to 'string', it would still be accepted.
*/
protected m2(x: string[]): string { // no compiler warning
return x.join(',');
}
}
const b = new B();
console.log(b.m1(['a', 'b', 'c'])); // ok
/**
* Here you are passing the type you have specified method m1 will take. So there is no issue here as well
*/
console.log(b.m1('d')); // runtime error
Everything said, this is a logical-error, rather than a bug.
Upvotes: 0