TypeScript Generics
A practical guide to TypeScript generics, type parameters, constraints, defaults, and reusable data structures.
TL;DR
- Declare reusable type placeholders using angle bracket
<T>parameter syntax. - Constrain type parameter shapes with the
extendskeyword operator. - Provide fallback defaults using
=in type parameter declarations.
Generic Functions
type parameter syntaxIntroduces a placeholder type variable within angle brackets.
function identity<T>(value: T): T {
return value;
}call-site inferenceDeduces type arguments automatically from passed arguments.
const text = identity('hello'); // T inferred as string
const num = identity(42); // T inferred as numbermultiple type parametersAccepts distinct generic variables separated by commas.
function pair<K, V>(key: K, val: V): [K, V] {
return [key, val];
}
const entry = pair('port', 3000);Generic Interfaces and Types
generic interfaceParameterizes object shapes for versatile data handling.
interface ApiResponse<T> {
status: number;
data: T;
}
const res: ApiResponse<string[]> = {
status: 200,
data: ['alpha', 'beta']
};generic type aliasBuilds reusable container models with flexible payloads.
type Result<T, E = Error> =
| { success: true; value: T }
| { success: false; error: E };generic function signatureDefines callable generic contracts in interface methods.
interface Transformer {
transform<T>(input: T): T;
}Type Parameter Constraints
extends constraintRestricts type parameters to shapes matching a contract.
interface HasId {
id: string;
}
function logId<T extends HasId>(item: T): string {
return item.id;
}keyof constraintConstrains a parameter to valid property keys of another.
function getProp<T, K extends keyof T>(
obj: T,
key: K
): T[K] {
return obj[key];
}primitive constraintLimits generics strictly to numbers or string types.
function add<T extends number>(a: T, b: T): number {
return a + b;
}Default Type Parameters
default type assignmentSupplies fallback types when callers omit type arguments.
type Container<T = string> = {
content: T;
};
const defaultBox: Container = { content: 'text' };
const numBox: Container<number> = { content: 99 };dependent default typesDerives fallback defaults from preceding type parameters.
interface CacheEntry<T, K = string> {
key: K;
value: T;
}function parameter defaultAssigns default generic types in function declarations.
function createList<T = number>(): T[] {
return [];
}
const defaultList = createList(); // number[]Generic Classes
generic class blueprintShares type parameters across all instance methods.
class Queue<T> {
private data: T[] = [];
push(item: T) { this.data.push(item); }
pop(): T | undefined { return this.data.shift(); }
}instantiating generic classBinds specific types during object creation with new.
const messages = new Queue<string>();
messages.push('hello');
const nextMsg = messages.pop();static member limitationStatic methods cannot reference class type parameters.
class Stack<T> {
// static items: T[]; // Error: Cannot reference T
static createEmpty<V>(): Stack<V> {
return new Stack<V>();
}
}Tips
- Let TypeScript infer generic
<T>type arguments from parameter values at call sites whenever possible. - Name type parameters descriptively like
TItemorTResponsewhen complex functions handle multiple distinct generic parameters.
Warnings
- Avoid declaring complex
<T>generic signatures when a simpler primitive union type fulfills the exact same role. - Remember that type parameters cannot be used as runtime values inside expressions like
new T()without constructor signatures.
In Practice
Demonstrates generic classes, constraints, and method return shapes in an in-memory data cache.
- Declare a base entity interface requiring an immutable string identifier.
- Author a generic repository class constrained to the base entity type.
- Implement CRUD methods that return exact typed instances rather than any.
- Instantiate the generic store for a concrete user entity and execute queries.
interface Identifiable {
id: string;
}
class EntityStore<T extends Identifiable> {
private items = new Map<string, T>();
set(entity: T) {
this.items.set(entity.id, entity);
}
get(id: string): T | undefined {
return this.items.get(id);
}
}
interface User extends Identifiable {
email: string;
}
const store = new EntityStore<User>();
store.set({ id: 'u1', email: 'dev@example.com' });
console.log(store.get('u1')?.email);FAQ
Use generics when you need to preserve the relationship between function input types and return values. While any erases type safety and permits invalid operations, generics like function identity guarantee that the returned value retains its exact original type.
When invoking a generic function, the compiler inspects the runtime values passed into parameters and matches them against type parameter positions. For example, passing [1, 2] to function first automatically infers T as number without requiring explicit angle brackets.
Yes, you can assign default types using an equals sign, such as . When consumers omit the type argument and the compiler cannot infer it from parameters, TypeScript falls back to the declared default.
type ResponseBox<T = string> = { data: T };
const box: ResponseBox = { data: 'hello' };TypeScript type parameters are erased at compile time and do not exist as constructors at runtime. To instantiate a generic type inside a function, require a constructor signature parameter like ctor: new () => T and invoke new ctor() directly.