Reputation: 7367
The following code is clearly incorrectly typed, yet I cannot make TypeScript error on it. I turned on strict
, as well as strictNullChecks
and strictFunctionTypes
for good measure, but TS remains unwavering in its conviction that this code is fine and dandy.
abstract class A {
// You can pass an undefined to any A
public abstract foo(_x: number | undefined);
}
class B extends A {
// B is an A, but it prohibits passing in an undefined.
// Note that if we did `x: string`, TS would flag it as
// an error.
public foo(x: number) {
if (x === undefined) {
throw new Error("Type error!");
}
}
}
function makeAnA(): A {
// This typechecks correct, so B is clearly an A, in
// TS's opinion.
return new B();
}
function test() {
const b = makeAnA();
// b is a B, so this should not be possible
b.foo(undefined);
}
Is this expected behavior? Is there some option I can turn on that will flag this as an error? I've had this bite me more than once.
Upvotes: 5
Views: 141
Reputation: 250156
This is a design decision. All method parameters behave bivariantly. This means that as far as ts is concerned, for methods (_x: number) => void
is a subtype of to (_x: number | number) => void
(and vice-versa). This is obviously unsound.
Initially not only method parameters behaved bivariantly but all function signature parameters. To fix this, the strictFuctionTypes
flag was added in typescript 2.6. From the PR:
With this PR we introduce a --strictFunctionTypes mode in which function type parameter positions are checked contravariantly instead of bivariantly. The stricter checking applies to all function types, except those originating in method or constructor declarations. Methods are excluded specifically to ensure generic classes and interfaces (such as Array) continue to mostly relate covariantly. The impact of strictly checking methods would be a much bigger breaking change as a large number of generic types would become invariant (even so, we may continue to explore this stricter mode).
(highlight added)
So here we get a glimpse into the decision to have methods parameters continue to relate bivariantly. It's for convenience. Without this unsoundness, most classes would be invariant. For example if Array
were invariant, Array<Dog>
would not be a subtype of Array<Animal>
, creating all sort of pain points in pretty basic code.
While definitely not equivalent, if we use a function field instead of a method (with strictFunctionTypes
turned on), we do get an error that Type '(x: number) => void' is not assignable to type '(_x: number | undefined) => void'
abstract class A {
// You can pass an undefined to any A
public foo!: (_x: number | undefined) => void;
}
class B extends A {
// Error here
public foo: (x: number) => void = x => {
if (x === undefined) {
throw new Error("Type error!");
}
}
}
function makeAnA(): A {
//And here
return new B();
}
function test() {
const b = makeAnA();
// b is a B, so this should not be possible
b.foo(undefined);
}
Note: The code above gives an error only with strictFunctionTypes
as without it all function parameter continue to behave bivariantly.
Upvotes: 3