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:
function compose<A, B, C>(f: (arg: A) => B, g: (arg: B) => C): (arg: A) => C {
return x => g(f(x));
}
compose
takes two other functions:
f
which takes some argument (of typeA
) and returns a value of typeB
g
which takes an argument of typeB
(the typef
returned), and returns a value of typeC
compose
then returns a function which feeds its argument throughf
and theng
.
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:
interface Person {
name: string;
age: number;
}
function getDisplayName(p: Person) {
return p.name.toLowerCase();
}
function getLength(s: string) {
return s.length;
}
// has type '(p: Person) => number'
const getDisplayNameLength = compose(
getDisplayName,
getLength,
);
// works and returns the type 'number'
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.
interface Box<T> {
value: T;
}
function makeArray<T>(x: T): T[] {
return [x];
}
function makeBox<U>(value: U): Box<U> {
return { value };
}
// has type '(arg: {}) => Box<{}[]>'
const makeBoxedArray = compose(
makeArray,
makeBox,
)
makeBoxedArray("hello!").value[0].toUpperCase();
// ~~~~~~~~~~~
// 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
(arg: {}) => Box<{}[]>
TypeScript 3.4 produces the type
<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!
interface Box<T> {
value: T;
}
function makeArray<T>(x: T): T[] {
return [x];
}
function makeBox<U>(value: U): Box<U> {
return { value };
}
// has type '<T>(arg: T) => Box<T[]>'
const makeBoxedArray = compose(
makeArray,
makeBox,
)
// works with no problem!
makeBoxedArray("hello!").value[0].toUpperCase();
For more details, you can read more at the original change.