In the previous part we saw that <T> can be anything. Sometimes, that is too flexible.
What if we want T to have a .length property yes or yes?
Constraints (extends)
Imagine this function:
function getLength<T>(item: T) {
return item.length; // ❌ Error: Property 'length' does not exist on type 'T'.
}
// TS complains because T could be a number (which doesn't have .length),
// or a boolean...function getLength<T>(item: T) {
return item.length; // ❌ Error: Property 'length' does not exist on type 'T'.
}
// TS complains because T could be a number (which doesn't have .length),
// or a boolean...To fix it, we use extends to put a restriction (Constraint).
// We say: "T can be whatever you want, BUT it must extend (fulfill) this shape"
interface HasLength {
length: number;
}
function getLength<T extends HasLength>(item: T) {
return item.length; // ✅ Now TS knows it exists
}
getLength("Hello"); // ✅ string has .length
getLength([1, 2, 3]); // ✅ array has .length
getLength(123); // ❌ Error: number does not have .length// We say: "T can be whatever you want, BUT it must extend (fulfill) this shape"
interface HasLength {
length: number;
}
function getLength<T extends HasLength>(item: T) {
return item.length; // ✅ Now TS knows it exists
}
getLength("Hello"); // ✅ string has .length
getLength([1, 2, 3]); // ✅ array has .length
getLength(123); // ❌ Error: number does not have .lengthGenerics with keyof (The master trick)
We will see this pattern everywhere. We want a function that gets a property from an object safely.
We need to define two Generics:
T: The object.K: The key. ButKcannot be any string, it must be a key of T.
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
const developer = {
name: "AliceDev",
language: "TypeScript"
};
getProperty(developer, "name"); // ✅ TS knows this returns string
getProperty(developer, "age"); // ❌ Error: "age" is not a key of developerfunction getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
const developer = {
name: "AliceDev",
language: "TypeScript"
};
getProperty(developer, "name"); // ✅ TS knows this returns string
getProperty(developer, "age"); // ❌ Error: "age" is not a key of developerDefault Values (=)
Just like in normal functions (function sum(a, b = 0)), Generics can have default values.
This is very useful to not force the user to write the type always.
// If we don't pass T, I'll assume it's 'string'
interface ApiResponse<T = string> {
data: T;
status: number;
}
// Usage without arguments:
const simple: ApiResponse = { data: "Hello", status: 200 }; // T is string
// Usage with arguments:
const complex: ApiResponse<number> = { data: 123, status: 200 }; // T is number// If we don't pass T, I'll assume it's 'string'
interface ApiResponse<T = string> {
data: T;
status: number;
}
// Usage without arguments:
const simple: ApiResponse = { data: "Hello", status: 200 }; // T is string
// Usage with arguments:
const complex: ApiResponse<number> = { data: 123, status: 200 }; // T is numberLet’s try it!
Let’s create a function merge that takes two objects obj1 and obj2 and combines them. Let’s type the function using two generics T and U.