TypeScript Generics Explained Simply
TypeScript Generics Explained Simply
Generics are one of TypeScript's most powerful features, and also one of the most intimidating for developers coming from JavaScript. Once you understand them, you will use them constantly — they are what makes TypeScript's type system genuinely expressive rather than just a thin layer of annotations. This article explains generics from first principles with practical examples.
The Problem Generics Solve
Imagine you want to write a function that returns the first element of an array. Without generics:
function first(arr: number[]): number {
return arr[0];
}
This works, but only for number[]. You would need separate functions for string[], boolean[], and so on. You could use any[], but then you lose all type information — the return type becomes any and TypeScript cannot help you.
Generics solve this by letting you write one function that works for any type while preserving type information:
function first<T>(arr: T[]): T {
return arr[0];
}
const n = first([1, 2, 3]); // n is number
const s = first(['a', 'b', 'c']); // s is string
The <T> declares a type parameter. TypeScript infers T from the argument you pass — no need to specify it manually in most cases.
Generic Interfaces
Generics are not limited to functions. They work on interfaces and type aliases:
interface ApiResponse<T> {
data: T;
error: string | null;
status: number;
}
type UserResponse = ApiResponse<User>;
type PostsResponse = ApiResponse<Post[]>;
Now UserResponse.data is typed as User and PostsResponse.data as Post[], while both share the same error and status fields.
Generic Classes
class Repository<T extends { id: string }> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
findById(id: string): T | undefined {
return this.items.find(item => item.id === id);
}
getAll(): T[] {
return [...this.items];
}
}
const userRepo = new Repository<User>();
const postRepo = new Repository<Post>();
Constraints with extends
Notice T extends { id: string } in the example above. This is a constraint — it tells TypeScript that T must have at least an id: string property. Without it, item.id inside the class would be a type error because TypeScript does not know that T has an id field.
Constraints let you access properties on the type parameter:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'Alice', age: 30 };
const name = getProperty(user, 'name'); // string
const age = getProperty(user, 'age'); // number
// getProperty(user, 'email') — compile error: 'email' doesn't exist
K extends keyof T means K must be one of the keys of T. This pattern is used throughout TypeScript's standard library.
Default Type Parameters
Like function parameters, generic type parameters can have defaults:
interface PaginatedResult<T, Meta = Record<string, unknown>> {
items: T[];
total: number;
meta: Meta;
}
type SimpleResult<T> = PaginatedResult<T>; // Meta defaults to Record<string, unknown>
Multiple Type Parameters
You can have as many type parameters as you need:
function zip<A, B>(a: A[], b: B[]): [A, B][] {
return a.map((item, i) => [item, b[i]]);
}
const pairs = zip([1, 2, 3], ['a', 'b', 'c']);
// pairs: [number, string][]
Conditional Types
Advanced generics include conditional types, which behave like ternary operators for types:
type NonNullable<T> = T extends null | undefined ? never : T;
type A = NonNullable<string | null>; // string
type B = NonNullable<number | undefined>; // number
NonNullable is actually a built-in TypeScript utility type implemented exactly this way.
Practical Utility Types
TypeScript ships with many generic utility types. The most useful:
Partial<T> // All properties optional
Required<T> // All properties required
Readonly<T> // All properties readonly
Pick<T, K> // Subset of properties
Omit<T, K> // All properties except K
Record<K, V> // Object with keys K and values V
ReturnType<F> // Return type of a function
Parameters<F> // Parameter types of a function as a tuple
Conclusion
Generics are the mechanism that makes TypeScript's type system composable. Instead of duplicating types or falling back to any, generics let you write flexible, reusable code that the compiler can still reason about precisely. Start by recognizing the pattern — angle brackets declare type parameters, extends adds constraints — and you will quickly learn to read and write generic code fluently.
Sign in to like, dislike, or report.