Strict function types

TypeScript 2.6 introduces a new strict checking flag, —strictFunctionTypes.The —strictFunctionTypes switch is part of the —strict family of switches, meaning that it defaults to on in —strict mode.You can opt-out by setting —strictFunctionTypes false on your command line or in your tsconfig.json.

Under —strictFunctionTypes function type parameter positions are checked contravariantly instead of bivariantly.For some background on what variance means for function types check out What are covariance and contravariance?.

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<T>) continue to mostly relate covariantly.

Consider the following example in which Animal is the supertype of Dog and Cat:

  1. declare let f1: (x: Animal) => void;
  2. declare let f2: (x: Dog) => void;
  3. declare let f3: (x: Cat) => void;
  4. f1 = f2; // Error with --strictFunctionTypes
  5. f2 = f1; // Ok
  6. 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.

Example

  1. interface Comparer<T> {
  2. compare: (a: T, b: T) => number;
  3. }
  4. declare let animalComparer: Comparer<Animal>;
  5. declare let dogComparer: Comparer<Dog>;
  6. animalComparer = dogComparer; // Error
  7. 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.

Note:

Under —strictFunctionTypes the first assignment is still permitted if compare was declared as a method.Effectively, T is bivariant in Comparer<T> because it is used only in method parameter positions.

  1. interface Comparer<T> {
  2. compare(a: T, b: T): number;
  3. }
  4. declare let animalComparer: Comparer<Animal>;
  5. declare let dogComparer: Comparer<Dog>;
  6. animalComparer = dogComparer; // Ok because of bivariance
  7. dogComparer = animalComparer; // Ok

TypeScript 2.6 also improves type inference involving contravariant positions:

  1. function combine<T>(...funcs: ((x: T) => void)[]): (x: T) => void {
  2. return x => {
  3. for (const f of funcs) f(x);
  4. }
  5. }
  6. function animalFunc(x: Animal) {}
  7. function dogFunc(x: Dog) {}
  8. 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.