DAY 22 OF 30
Generic constraints
Yesterday, in one sentence
A generic function or type declares a type parameter in angle brackets, which stands in for a type that is decided when the code is used. It means one definition works with many types, instead of repeating it for each one.
Did you say:
- ✓a type parameter declared in angle brackets
- ✓stands in for a type decided when the code is used
- ✓one definition works with many types
Interview question
Declare a type parameter in angle brackets after the function name, then use it in the parameters and the return type. TypeScript infers what it is from the argument when the function is called.
Did you say:
- ✓declare a type parameter in angle brackets after the function name
- ✓use it in the parameters and the return type
- ✓TypeScript infers it from the argument
A type parameterType Parameter can be any type at all, so inside the function you cannot assume anything about the type.
function logName<T>(character: T) {
console.log(character.name);
// ❌ Property 'name' does not exist on type 'T'
}
T might be a number for example, and numbers do not have a name property.
A generic constraint limits which types the type parameter is allowed to be. You
write it with the extends keyword.
type Hero = { name: string };
function logName<T extends Hero>(character: T) {
console.log(character.name); // ok, every T has a name
}
logName({ name: "Logan", claws: true }); // ok
logName({ claws: true });
// ❌ Argument is missing the property 'name'
T extends Hero means T can be any type that has a name property of type
string, which in practice means an object type. Additional properties like claws
do not cause an error, because this is structural typingStructural Typing: having the required
properties is enough.
The reason to use a constrained type parameter rather than annotating the parameter as
Hero is what happens to the return typeReturn Type.
type Hero = { name: string };
function annotated(character: Hero) {
return character;
}
const a = annotated({ name: "Logan", claws: true });
// a is of type Hero, claws has been lost
function generic<T extends Hero>(character: T): T {
return character;
}
const b = generic({ name: "Logan", claws: true });
// b is { name: string; claws: boolean }
The annotation replaces the type with Hero. The type parameter keeps whatever was
passed in.
How extends works
extends checks whether one type is the same as another or narrower than it. A
narrower type is one that fits inside a wider one.
"wolverine" is narrower than string, because string contains every string,
including "wolverine". string is narrower than unknown, because unknown
contains every type, including string.
type A = "wolverine" extends string ? true : false; // true
type B = string extends unknown ? true : false; // true
type C = unknown extends "wolverine" ? true : false; // false
Check yourself
function getLength<T extends { length: number }>(item: T): number {
return item.length
}
A number has no length property, so it does not satisfy the constraint. Strings and arrays both have length.
A generic constraint sets a limit on the types that can be passed to a type
parameter, written with extends. The constraint lets you use the properties
defined on it inside the function, because every allowed type must have those
properties.
Did you say:
- ✓sets a limit on the types that can be passed to a type parameter
- ✓written with `extends`
- ✓lets you use the constraint's properties inside the function