Typescript generic : T vs T extends {} Difference and Examples
Typescript generic type parameters: T vs T extends {}
TLDR: "T extend {}" allow you your function or class' method to accept pretty much anything except null and undefined. T can be a primitive though.
On the other hand, only T, allow accepting also those value, null and undefined
I do work with Typescript in a production application, though I started to learn yet
way before, since more than a year now, and as with anything that one learns there are
many aspects, at some point one starts to dig into details and advanced concept.
This is the case for today's subject. I have found myself writing a code:
export class Attributes<T> {
constructor(private data: T) {}
get<K extends keyof T>(key: K): T[K] {
// this make data be an object
return this.data[key];
}
set(update: T): void {
Object.assign(this.data, update);
}
}
in which the Typescript compiler under the 'this.data' object throw the following:
No overload matches this call.
Overload 1 of 4, '(target: {}, source: T): {} & T', gave the following error.
Argument of type 'T' is not assignable to parameter of type '{}'.
Overload 2 of 4, '(target: object, ...sources: any[]): any', gave the following error.
Argument of type 'T' is not assignable to parameter of type 'object'.ts(2769)
Attributes.ts(7, 25): This type parameter might need an `extends {}` constraint.
Attributes.ts(7, 25): This type parameter might need an `extends object` constraint.
(property) Attributes<T>.data: T
At the beginning, I did not I understood, because the tutorial I was reviewing is quite advanced and updated, then after some research, I find out that things did change a bit after version 3.5 of Typescript.
In TypeScript 3.5 a change was made so that generic type parameters are implicitly constrained by unknown instead of the empty object type {}, this means that
function funcA<T>() { }
function funcB<T extends {}>() {}
there are some minor details about the difference between funcA() and funcB().
These differences from TS 3.5 are the following:
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 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 to make it more clear Compare:
function funcA<T>() { }
funcA<undefined>(); // okay
funcA<null>(); // okay
funcA<string>(); // okay
funcA<{ a: string }>(); // okay
Meanwhile
function funcB<T extends {}>() { }
funcB<undefined>(); // error
funcB<null>(); // error
funcB<string>(); // okay
funcB<{ a: string }>(); // okay
So if one needs to avoid null and undefined passed as value then T extends {} is the one to use.
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
I will follow with more example and some codesandbox.