Higher order type inference from generic functions

TypeScript 3.4 can now produce generic function types when inference from other generic functions produces free type variables for inferences.This means many function composition patterns now work better in 3.4.

To get more specific, let’s build up some motivation and consider the following compose function:

  1. function compose<A, B, C>(f: (arg: A) => B, g: (arg: B) => C): (arg: A) => C {
  2. return x => g(f(x));
  3. }

compose takes two other functions:

  • f which takes some argument (of type A) and returns a value of type B
  • g which takes an argument of type B (the type f returned), and returns a value of type Ccompose then returns a function which feeds its argument through f and then g.

When calling this function, TypeScript will try to figure out the types of A, B, and C through a process called type argument inference.This inference process usually works pretty well:

  1. interface Person {
  2. name: string;
  3. age: number;
  4. }
  5. function getDisplayName(p: Person) {
  6. return p.name.toLowerCase();
  7. }
  8. function getLength(s: string) {
  9. return s.length;
  10. }
  11. // has type '(p: Person) => number'
  12. const getDisplayNameLength = compose(
  13. getDisplayName,
  14. getLength,
  15. );
  16. // works and returns the type 'number'
  17. getDisplayNameLength({ name: "Person McPersonface", age: 42 });

The inference process is fairly straightforward here because getDisplayName and getLength use types that can easily be referenced.However, in TypeScript 3.3 and earlier, generic functions like compose didn’t work so well when passed other generic functions.

  1. interface Box<T> {
  2. value: T;
  3. }
  4. function makeArray<T>(x: T): T[] {
  5. return [x];
  6. }
  7. function makeBox<U>(value: U): Box<U> {
  8. return { value };
  9. }
  10. // has type '(arg: {}) => Box<{}[]>'
  11. const makeBoxedArray = compose(
  12. makeArray,
  13. makeBox,
  14. )
  15. makeBoxedArray("hello!").value[0].toUpperCase();
  16. // ~~~~~~~~~~~
  17. // error: Property 'toUpperCase' does not exist on type '{}'.

In older versions, TypeScript would infer the empty object type ({}) when inferring from other type variables like T and U.

During type argument inference in TypeScript 3.4, for a call to a generic function that returns a function type, TypeScript will, as appropriate, propagate type parameters from generic function arguments onto the resulting function type.

In other words, instead of producing the type

  1. (arg: {}) => Box<{}[]>

TypeScript 3.4 produces the type

  1. <T>(arg: T) => Box<T[]>

Notice that T has been propagated from makeArray into the resulting type’s type parameter list.This means that genericity from compose’s arguments has been preserved and our makeBoxedArray sample will just work!

  1. interface Box<T> {
  2. value: T;
  3. }
  4. function makeArray<T>(x: T): T[] {
  5. return [x];
  6. }
  7. function makeBox<U>(value: U): Box<U> {
  8. return { value };
  9. }
  10. // has type '<T>(arg: T) => Box<T[]>'
  11. const makeBoxedArray = compose(
  12. makeArray,
  13. makeBox,
  14. )
  15. // works with no problem!
  16. makeBoxedArray("hello!").value[0].toUpperCase();

For more details, you can read more at the original change.