The approach taken here reflects a deliberate design choice. In this context, all method parameters exhibit bivariant behavior. This essentially means that within TypeScript, a method like (_x: number) => void
is considered a subtype of (_x: number | number) => void
(and vice versa). However, it's evident that this approach lacks robustness.
Initially, not only did method parameters showcase bivariance, but all function signature parameters did as well. To address this issue, the strictFunctionTypes
flag was introduced in typescript 2.6. As stated in the PR:
This PR introduces a --strictFunctionTypes mode in which function type parameter positions are examined contravariantly rather than bivariantly. This stricter checking rule applies to all function types except those originating from method or constructor declarations. Methods are exempted specifically to ensure generic classes and interfaces (such as Array) remain predominantly covariant. Strict method checking would bring about a significant breaking change due to a large number of generic types becoming invariant. Nevertheless, exploring this stricter mode may still be on the cards.
(highlight added)
Hence, the decision to maintain bivariant relationships for method parameters can be attributed to convenience. Without this leniency, most classes would end up being invariant. For instance, if Array
were invariant, Array<Dog>
would not qualify as a subtype of Array<Animal>
, leading to numerous challenges in basic code implementation.
Although not identical, when utilizing a function field instead of a method (alongside strictFunctionTypes
enabled), an error is triggered stating
Type '(x: number) > void' is not assignable to type '(_x: number | undefined) > void'
abstract class A {
// Capability to accept undefined values for any instance of A
public foo!: (_x: number | undefined) => void;
}
class B extends A {
// Error detected at this point
public foo: (x: number) => void = x => {
if (x === undefined) {
throw new Error("Type error!");
}
}
}
function createAnInstanceA(): A {
// And over here
return new B();
}
function verify() {
const b = makeAnA();
// Since b represents a B object, this operation should not be valid
b.foo(undefined);
}
Playground Link
Note: The code snippet above generates an error exclusively with strictFunctionTypes
enabled, as without it, all function parameters continue to behave bivariantly.