TypeScript course menu
TypeScript Generic Constraints and Defaults: extends, keyof and Default Type Parameters
Learn to restrict and configure TypeScript generics: constraints with extends, requiring properties, keyof constraints for type-safe property access, default type parameters, const type parameters, and common mistakes with generics.
On this page
A plain type parameter T can be any type, so inside a generic function you cannot assume anything about it. Constraints limit which types are allowed, which in turn lets you use their features.
The problem#
function longest<T>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}error TS2339: Property 'length' does not exist on type 'T'.TypeScript cannot know that every possible T has a length.
extends: constraining a type parameter#
T extends Something means "T can be any type, as long as it is assignable to Something":
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
console.log(longest("apple", "fig"));
console.log(longest([1, 2], [1, 2, 3]));apple
[ 1, 2, 3 ]Strings and arrays both have a length, so both work, and the result keeps the precise type (string or number[]). Types without length are rejected:
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest(10, 20);error TS2345: Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.Constraining to object shapes#
interface HasId {
id: number;
}
function findById<T extends HasId>(items: T[], id: number): T | undefined {
return items.find((item) => item.id === id);
}
const products = [
{ id: 1, name: "Pen", price: 20 },
{ id: 2, name: "Bag", price: 1500 },
];
const bag = findById(products, 2); // type: { id: number; name: string; price: number } | undefined
console.log(bag?.name, bag?.price);Bag 1500The constraint guarantees id exists, while the return type keeps all the properties of the actual items (name, price). With a parameter typed simply as HasId[], the result would only be known to have id.
keyof constraints: type-safe property access#
K extends keyof T restricts K to the property names of T:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Asha", age: 21, active: true };
const name = getProperty(user, "name"); // type: string
const age = getProperty(user, "age"); // type: number
console.log(name.toUpperCase(), age + 1);ASHA 22function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Asha", age: 21 };
getProperty(user, "email");error TS2345: Argument of type '"email"' is not assignable to parameter of type '"name" | "age"'.T[K] is an indexed access type: the type of property K in T. Both keyof and indexed access are explained in keyof, typeof and Indexed Access. This pattern powers type-safe sorting, plucking and form libraries:
function sortBy<T, K extends keyof T>(items: T[], key: K): T[] {
return [...items].sort((a, b) => (a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0));
}
const people = [
{ name: "Ravi", age: 31 },
{ name: "Asha", age: 25 },
];
console.log(sortBy(people, "age").map((p) => p.name));
console.log(sortBy(people, "name").map((p) => p.name));[ 'Asha', 'Ravi' ]
[ 'Asha', 'Ravi' ]Default type parameters#
Type parameters can have defaults, used when no type argument is given and none can be inferred:
interface ApiResponse<T = unknown> {
ok: boolean;
data: T;
}
const untyped: ApiResponse = { ok: true, data: { anything: 1 } }; // data: unknown
const typed: ApiResponse<string[]> = { ok: true, data: ["HTML"] };
console.log(typeof untyped.data, typed.data[0]);object HTMLDefaults can be combined with constraints: <T extends object = Record<string, unknown>>.
const type parameters#
By default, inference widens literal values ("GET" becomes string). A const modifier on a type parameter infers the narrowest types, as if the caller had written as const:
function defineRoutes<const T extends readonly string[]>(routes: T): T {
return routes;
}
const routes = defineRoutes(["/home", "/courses"]); // readonly ["/home", "/courses"]
type Route = (typeof routes)[number]; // "/home" | "/courses"
const r: Route = "/courses";
console.log(r, routes.length);/courses 2Common mistakes#
Practice#
Practice exercise
Write pluck<T, K extends keyof T>(items: T[], key: K): T[K][] that returns the values of one property from every item, and sumBy<T>(items: T[], getValue: (item: T) => number): number. Use them on an array of orders.
Show solution
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map((item) => item[key]);
}
function sumBy<T>(items: T[], getValue: (item: T) => number): number {
return items.reduce((total, item) => total + getValue(item), 0);
}
const orders = [
{ id: 1, customer: "Asha", amount: 499 },
{ id: 2, customer: "Ravi", amount: 1200 },
];
console.log(pluck(orders, "customer"));
console.log(sumBy(orders, (o) => o.amount));[ 'Asha', 'Ravi' ]
1699Key takeaway#
Finished this lesson?