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?
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!