Strict function types by ahejlsberg · Pull Request #18654 · microsoft/TypeScript (original) (raw)
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 construcor declarations. Methods are excluded specifically to ensure generic classes and interfaces (such as Array<T>
) 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).
The --strictFunctionTypes
switch is part of the --strict
family of switches, meaning that it defaults to on in --strict
mode. This PR is therefore a breaking change only in --strict
mode.
Consider the following example in which Animal
is the supertype of Dog
and Cat
:
declare let f1: (x: Animal) => void; declare let f2: (x: Dog) => void; declare let f3: (x: Cat) => void; f1 = f2; // Error with --strictFunctionTypes f2 = f1; // Ok f2 = f3; // Error
The first assignment is permitted in default type checking mode, but flagged as an error in strict function types mode. Intuitively, the default mode permits the assignment because it is possibly sound, whereas strict function types mode makes it an error because it isn't provably sound. In either mode the third assignment is an error because it is never sound.
Another way to describe the example is that the type (x: T) => void
is bivariant (i.e. covariant or contravariant) for T
in default type checking mode, but contravariant for T
in strict function types mode.
Another example:
interface Comparer { compare(a: T, b: T): number; }
declare let animalComparer: Comparer; declare let dogComparer: Comparer;
animalComparer = dogComparer; // Ok because of bivariance dogComparer = animalComparer; // Ok
In --strictFunctionTypes
mode the first assignment is still permitted because compare
is declared as a method. Effectively, T
is bivariant in Comparer<T>
because it is used only in method parameter positions. However, changing compare
to be a property with a function type causes stricter checking to take effect:
interface Comparer { compare: (a: T, b: T) => number; }
declare let animalComparer: Comparer; declare let dogComparer: Comparer;
animalComparer = dogComparer; // Error dogComparer = animalComparer; // Ok
The first assignment is now an error. Effectively, T
is contravariant in Comparer<T>
because it is used only in function type parameter positions.
By the way, note that whereas some languages (e.g. C# and Scala) require variance annotations (out
/in
or +
/-
), variance emerges naturally from the actual use of a type parameter within a generic type due to TypeScript's structural type system.
We also improve type inference involving contravariant positions in this PR:
function combine(...funcs: ((x: T) => void)[]): (x: T) => void { return x => { for (const f of funcs) f(x); } }
function animalFunc(x: Animal) {} function dogFunc(x: Dog) {}
let combined = combine(animalFunc, dogFunc); // (x: Dog) => void
Above, all inferences for T
originate in contravariant positions, and we therefore infer the best common subtype for T
. This contrasts with inferences from covariant positions, where we infer the best common supertype. Note that inferences from covariant positions have precedence over inferences from contravariant positions.
Fixes #6102.
Fixes #7472.
Fixes #9514.
Fixes #9765.
Fixes #12498.
Fixes #13248.
Fixes #15579.
Fixes #18337.
Fixes #18466.