Question

Typescript generic type parameters: T vs T extends {}

Is there functionally any difference between the two generic type parameters below?.

function funcA<T>() { }
function funcB<T extends {}>() {}

I have seen them both used and am confused as to the differences?

 46  59567  46
1 Jan 1970

Solution

 57

Note: I'll assume you're using a version of TypeScript 3.5 or later; in TypeScript 3.5 a change was made so that generic type parameters are implicitly constrained by unknown instead of the empty object type {}, and some minor details about the difference between funcA() and funcB() changed. I don't want to make a long post even longer by talking about how things used to be in TS3.4 and below.


If you don't explicitly constrain a generic type parameter via extends XXX, then it will implicitly be constrained by unknown, the "top type" to which all types are assignable. So in practice that means the T in funcA<T>() could be absolutely any type you want.

On the other hand, the empty object type {}, is a type to which nearly all types are assignable, except for null and undefined, when you have enabled the --strictNullChecks compiler option (which you should). Even primitive types like string and number are assignable to {}.

So compare:

function funcA<T>() { }
funcA<undefined>(); // okay
funcA<null>(); // okay
funcA<string>(); // okay
funcA<{ a: string }>(); // okay

with

function funcB<T extends {}>() { }
funcB<undefined>(); // error
funcB<null>(); // error
funcB<string>(); // okay
funcB<{ a: string }>(); // okay

The only difference is that T extends {} forbids null and undefined.


It might be a little confusing that {}, a so-called "object" type, would accept primitives like string and number. It helps to think of such curly-brace-surrounded types like {} and {a: string} as well as all interface types not necessarily as "true" object types, but as types of values where you can index into them as if they were objects without getting runtime errors. Primitives except for null and undefined are "object-like" in that you can treat them as if they were wrapped with their object equivalents:

const s: string = "";
s.toUpperCase(); // okay

And therefore even primitives like string are assignable to curly-brace-surrounded types as long as the members of those types match:

const x: { length: number } = s; // okay

If you really need to express a type that only accepts "true", i.e., non-primitive objects, you can use the object:

const y: object & { length: number } = s; // error
const z: object & { length: number } = { length: 10 }; // okay

But I (seriously) digress.


Okay, hope that helps; good luck!

Playground link to code

2020-05-07

Solution

 7

Yes. In funcB, T must extend {} which means pretty much anything except null and undefined. T can be a primitive though.

2020-05-07