Generics are the feature most TypeScript developers underuse. Once you understand them deeply, you'll write code that's both more flexible and more type-safe simultaneously.
What Are Generics?
A generic is a placeholder for a type that gets filled in at call time. Think of it like a function parameter, but for types:
// Without generics — loses type information
function identity(value: any): any {
return value;
}
// With generics — type is preserved
function identity<T>(value: T): T {
return value;
}
const result = identity(42); // result: number ✅
const str = identity("hello"); // str: string ✅
Generic Constraints
Use extends to restrict what types a generic can be:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30 };
getProperty(user, "name"); // string ✅
getProperty(user, "age"); // number ✅
getProperty(user, "role"); // ❌ Error: "role" not in type
Conditional Types
This is where things get powerful. Conditional types let you create types that change based on conditions:
type IsArray<T> = T extends any[] ? true : false;
type A = IsArray<string[]>; // true
type B = IsArray<number>; // false
The infer Keyword
infer lets you extract a type from within a conditional type:
// Extract the return type of any function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Result = ReturnType<() => Promise<string>>; // Promise<string>
// Unwrap a Promise
type Awaited<T> = T extends Promise<infer R> ? Awaited<R> : T;
type Resolved = Awaited<Promise<Promise<number>>>; // number
[!TIP]
inferonly works inside theextendsclause of a conditional type. You can use it multiple times in a single conditional to extract multiple type parameters.
Mapped Types
Mapped types transform every property of a type:
// Make all properties optional (like TypeScript's built-in Partial<T>)
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Make all properties readonly
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// Transform values
type Stringify<T> = {
[K in keyof T]: string;
};
Remapping Keys
TypeScript 4.1+ allows remapping keys with as:
// Add a "get" prefix to every key
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type User = { name: string; age: number };
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number }
Real-World Pattern: Type-Safe API Response
Here's a practical generic pattern for API responses:
type ApiResponse<T> =
| { success: true; data: T; error: null }
| { success: false; data: null; error: string };
async function fetchUser(id: string): Promise<ApiResponse<User>> {
try {
const user = await db.users.findById(id);
return { success: true, data: user, error: null };
} catch (e) {
return { success: false, data: null, error: (e as Error).message };
}
}
// Usage — TypeScript narrows the type automatically
const result = await fetchUser("123");
if (result.success) {
console.log(result.data.name); // User ✅
} else {
console.log(result.error); // string ✅
}
Built-in Utility Types
TypeScript ships with these generic utility types — learn them:
| Type | Description |
|---|---|
Partial<T> | All properties optional |
Required<T> | All properties required |
Readonly<T> | All properties readonly |
Pick<T, K> | Keep only keys K |
Omit<T, K> | Remove keys K |
Record<K, V> | Object with keys K and values V |
ReturnType<T> | Return type of a function |
Awaited<T> | Unwrap a Promise |
NonNullable<T> | Remove null and undefined |
Summary
Generics unlock a level of type safety that makes your code self-documenting and refactor-proof. The pattern progression:
- Basic generics —
<T>as a type placeholder - Constraints —
extendsto restrict T - Conditional types —
T extends X ? A : B infer— extract types from patterns- Mapped types — transform every key of T
Start using them in your API layer and you'll never want to go back to any.
Written by
DebuggerMe TeamThe DebuggerMe team builds developer tools, writes technical content, and helps teams ship better software.
Related Articles
All articles →Getting Started with Next.js 16 — A Complete Guide
Everything you need to know to build fast, modern web applications with Next.js 16 App Router, Server Components, and TypeScript. From project setup to production deployment.
React Server Components in Depth — What They Are and When to Use Them
React Server Components fundamentally change how we think about rendering. This guide breaks down how they work, how they differ from Client Components, and the patterns that will make your Next.js apps faster.
Node.js 22 Is Here — Everything You Need to Know
Node.js 22 lands as the new LTS with native TypeScript type-stripping, a built-in test runner improvements, WebSocket client, and the much-anticipated require(esm) support. Here's a practical breakdown.