Skip to content
Laviyx Learn

Try

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.

Advanced8 min readUpdated
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#

TypeScript
function longest<T>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}
Compiler error
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":

TypeScript
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]));
Output
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:

TypeScript
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}
longest(10, 20);
Compiler error
error TS2345: Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.

Constraining to object shapes#

TypeScript
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);
Output
Bag 1500

The 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:

TypeScript
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);
Output
ASHA 22
TypeScript
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
const user = { name: "Asha", age: 21 };
getProperty(user, "email");
Compiler error
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:

TypeScript
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));
Output
[ 'Asha', 'Ravi' ]
[ 'Asha', 'Ravi' ]

Default type parameters#

Type parameters can have defaults, used when no type argument is given and none can be inferred:

TypeScript
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]);
Output
object HTML

Defaults 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:

TypeScript
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);
Output
/courses 2

Common 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
TypeScript
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));
Output
[ 'Asha', 'Ravi' ]
1699

Key takeaway#

Finished this lesson?