TL;DR
TypeScript generics are type-level functions: <T> is a parameter filled in at the call site. Use constraints (T extends X) to require properties; use conditional types (T extends U ? X : Y) to compute types at compile time; use mapped types to transform object shapes; use template literal types to type string patterns. Master the built-in utility types (Partial, Required, Record, ReturnType, Awaited) β they are all implemented with generics. Convert your TypeScript to JavaScript instantly with our TypeScript to JavaScript converter.
Generic Functions β Syntax, Inference, and Multiple Type Parameters
A generic function defers the decision about which type to use to the caller. The type parameter <T> is captured at the call site through type inference β you rarely need to pass it explicitly.
// Basic generic function
function identity<T>(arg: T): T {
return arg;
}
const str = identity('hello'); // T inferred as string
const num = identity(42); // T inferred as number
const explicit = identity<boolean>(true); // explicit type argument
// Multiple type parameters
function pair<A, B>(first: A, second: B): [A, B] {
return [first, second];
}
const p = pair('name', 42); // [string, number]
// Generic with return type inferred
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const head = first([1, 2, 3]); // number | undefined
// Generic arrow functions (TSX files need the trailing comma)
const mapArr = <T, U>(arr: T[], fn: (item: T) => U): U[] => arr.map(fn);
// Generic with default type parameter (TS 2.3+)
function createState<T = string>(initial: T): { value: T; set: (v: T) => void } {
let value = initial;
return { value, set: (v) => { value = v; } };
}
const s1 = createState(); // T defaults to string
const s2 = createState(0); // T inferred as numberGeneric Interfaces and Classes β Stack, Queue, Repository
Generic interfaces and classes allow you to create reusable data structures and services that work with any type while maintaining full type safety.
// Generic interface
interface Repository<T, ID = string> {
findById(id: ID): Promise<T | null>;
findAll(): Promise<T[]>;
save(entity: T): Promise<T>;
delete(id: ID): Promise<void>;
}
// Generic Stack<T>
class Stack<T> {
private items: T[] = [];
push(item: T): void { this.items.push(item); }
pop(): T | undefined { return this.items.pop(); }
peek(): T | undefined { return this.items[this.items.length - 1]; }
isEmpty(): boolean { return this.items.length === 0; }
size(): number { return this.items.length; }
}
const numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
const top = numStack.pop(); // number | undefined
// Generic Queue<T>
class Queue<T> {
private items: T[] = [];
enqueue(item: T): void { this.items.push(item); }
dequeue(): T | undefined { return this.items.shift(); }
front(): T | undefined { return this.items[0]; }
size(): number { return this.items.length; }
}
// User type for examples
interface User { id: number; name: string; email: string; }
// Generic Repository implementation
class UserRepository implements Repository<User, number> {
private store = new Map<number, User>();
async findById(id: number): Promise<User | null> {
return this.store.get(id) ?? null;
}
async findAll(): Promise<User[]> { return [...this.store.values()]; }
async save(user: User): Promise<User> {
this.store.set(user.id, user);
return user;
}
async delete(id: number): Promise<void> { this.store.delete(id); }
}
// Generic class with constructor constraint
function createInstance<T>(ctor: new () => T): T {
return new ctor();
}
class Dog { bark() { return 'woof'; } }
const dog = createInstance(Dog); // Dog β fully typedConstraints β extends, keyof, and Bounded Type Parameters
Constraints narrow what types are acceptable for a type parameter. The extends keyword creates an upper bound.
// Basic constraint β T must have a length property
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest('hello', 'hi'); // string β OK
longest([1, 2, 3], [4, 5]); // number[] β OK
// longest(1, 2); // Error: number has no 'length'
// keyof constraint β K must be a key of T
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const userObj = { name: 'Alice', age: 30 };
const uname = getProperty(userObj, 'name'); // string
const uage = getProperty(userObj, 'age'); // number
// getProperty(userObj, 'email'); // Error: not a key
// T extends object β exclude primitives
function objectKeys<T extends object>(obj: T): (keyof T)[] {
return Object.keys(obj) as (keyof T)[];
}
// T extends keyof U β key of another type parameter
function pickFields<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
return keys.reduce((acc, key) => {
acc[key] = obj[key];
return acc;
}, {} as Pick<T, K>);
}
// Multiple constraints using intersection
function mergeObjects<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b };
}
// Constraint with constructor
function build<T>(ctor: new () => T): T {
return new ctor();
}Conditional Types β infer, Distributive Behavior, and NonNullable
Conditional types compute a type based on a type-level condition: T extends U ? X : Y. They are evaluated at compile time and are the basis for most advanced utility types.
// Basic conditional type
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>; // false
// infer keyword β extract a type from a pattern
type MyReturnType<T> = T extends (...args: unknown[]) => infer R ? R : never;
type Fn = (x: number) => string;
type R = MyReturnType<Fn>; // string
type ElementType<T> = T extends (infer E)[] ? E : T;
type E1 = ElementType<number[]>; // number
type E2 = ElementType<string>; // string (not an array, returns T itself)
// Awaited β recursively unwrap promises
type MyAwaited<T> = T extends Promise<infer U> ? MyAwaited<U> : T;
type Resolved = MyAwaited<Promise<Promise<number>>>; // number
// Distributive conditional types β distribute over unions
type ToArray<T> = T extends unknown ? T[] : never;
type StrOrNumArr = ToArray<string | number>; // string[] | number[]
// Prevent distribution with wrapping in a tuple
type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;
type Combined = ToArrayNonDist<string | number>; // (string | number)[]
// NonNullable β removes null and undefined
type MyNonNullable<T> = T extends null | undefined ? never : T;
type Safe = MyNonNullable<string | null | undefined>; // string
// Complex infer β extract promise resolve type
type UnwrapPromise<T> =
T extends Promise<infer U> ? U :
T extends PromiseLike<infer U> ? U :
T;Mapped Types β Transform Object Shapes
Mapped types iterate over the keys of a type and produce a new object type. All of TypeScript's built-in structural utility types (Partial, Required, Readonly, Record) are mapped types.
// Built-in mapped types (their implementations)
type MyPartial<T> = { [K in keyof T]?: T[K] }; // all optional
type MyRequired<T> = { [K in keyof T]-?: T[K] }; // all required (-? removes ?)
type MyReadonly<T> = { readonly [K in keyof T]: T[K] }; // all readonly
type MyRecord<K extends string, V> = { [P in K]: V }; // key-value map
// Custom: make specific keys optional
type PartialBy<T, K extends keyof T> =
Omit<T, K> & Partial<Pick<T, K>>;
interface UserType { id: number; name: string; email: string; }
type UserUpdate = PartialBy<UserType, 'name' | 'email'>;
// { id: number; name?: string; email?: string }
// Custom: make all values nullable
type Nullable<T> = { [K in keyof T]: T[K] | null };
// Custom: deep partial
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
// Key remapping with 'as' clause (TS 4.1+)
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<UserType>;
// { getId: () => number; getName: () => string; getEmail: () => string }
// Filter keys by value type using 'as never'
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type StringFields = OnlyStrings<{ id: number; name: string; email: string }>;
// { name: string; email: string }
// Mutable<T> β remove readonly
type Mutable<T> = { -readonly [K in keyof T]: T[K] };Template Literal Types β String Pattern Typing
Template literal types (TypeScript 4.1+) construct string types using backtick syntax. When unions appear in template positions, TypeScript automatically distributes to produce all permutations.
// Basic template literal type
type Greeting = `Hello, ${string}`;
const g1: Greeting = 'Hello, Alice'; // OK
// Union distribution β cartesian product
type Direction = 'top' | 'right' | 'bottom' | 'left';
type CSSPadding = `padding-${Direction}`;
// "padding-top" | "padding-right" | "padding-bottom" | "padding-left"
// EventHandler<T> β typed event names
type EventHandler<T extends string> = `on${Capitalize<T>}`;
type ClickHandler = EventHandler<'click'>; // "onClick"
type InputEvents = EventHandler<'change' | 'blur' | 'focus'>;
// "onChange" | "onBlur" | "onFocus"
// API path pattern
type ApiVersion = 'v1' | 'v2';
type Resource = 'users' | 'posts' | 'comments';
type ApiPath = `/api/${ApiVersion}/${Resource}`;
// "/api/v1/users" | "/api/v1/posts" | ... (6 combinations)
// Intrinsic string manipulation types
type UpperHello = Uppercase<'hello'>; // "HELLO"
type LowerHELLO = Lowercase<'HELLO'>; // "hello"
type CapHello = Capitalize<'hello'>; // "Hello"
type UncapHello = Uncapitalize<'Hello'>; // "hello"
// Deep key paths with template literals
type Paths<T extends object, Prefix extends string = ''> = {
[K in keyof T & string]:
T[K] extends object
? Paths<T[K], `${Prefix}${K}.`> | `${Prefix}${K}`
: `${Prefix}${K}`;
}[keyof T & string];
type Config = { db: { host: string; port: number }; port: number };
type ConfigPaths = Paths<Config>;
// "db" | "db.host" | "db.port" | "port"Utility Types Deep Dive β Awaited, Parameters, ReturnType, InstanceType
TypeScript ships with a rich set of built-in generic utility types. Here are the ones most developers underutilize:
// Parameters<T> β tuple of function parameter types
type SomeFn = (a: string, b: number, c: boolean) => void;
type Params = Parameters<SomeFn>; // [string, number, boolean]
// ReturnType<T> β function return type
type ParseResult = ReturnType<typeof JSON.parse>; // any
interface UserType { id: string; name: string; }
declare function fetchUser(id: string): Promise<UserType>;
type FetchReturn = ReturnType<typeof fetchUser>; // Promise<UserType>
type ResolvedUser = Awaited<ReturnType<typeof fetchUser>>; // UserType
// ConstructorParameters<T> β constructor parameter types
class HttpClient {
constructor(public baseUrl: string, public timeout: number) {}
}
type CtorArgs = ConstructorParameters<typeof HttpClient>; // [string, number]
// InstanceType<T> β type returned by new T()
type ClientInstance = InstanceType<typeof HttpClient>; // HttpClient
// OmitThisParameter<T> β removes 'this' parameter
function greetUser(this: { name: string }, greeting: string): string {
return `${greeting}, ${this.name}!`;
}
type GreetFn = OmitThisParameter<typeof greetUser>; // (greeting: string) => string
// Awaited<T> β recursive promise unwrap (TS 4.5+)
type DeepPromise = Promise<Promise<Promise<number>>>;
type Flat = Awaited<DeepPromise>; // number
// Extract<T, U> and Exclude<T, U>
type Status = 'active' | 'inactive' | 'banned' | 'pending';
type ActiveStates = Extract<Status, 'active' | 'pending'>; // "active" | "pending"
type NegativeStates = Exclude<Status, 'active' | 'pending'>; // "inactive" | "banned"
// NonNullable removes null and undefined from a union
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>; // stringVariance β Covariance, Contravariance, and in/out Modifiers (TS 4.7)
Variance determines how subtype relationships flow through generic types. Understanding it prevents subtle type safety bugs.
// Setup: Animal <- Dog subtype hierarchy
class Animal { breathe(): void {} }
class Dog extends Animal { bark(): void {} }
// COVARIANCE β subtype relationship preserved
// Producer<Dog> is assignable to Producer<Animal>
interface Producer<out T> { get(): T; }
const produceDog: Producer<Dog> = { get: () => new Dog() };
const produceAnimal: Producer<Animal> = produceDog; // OK
// CONTRAVARIANCE β relationship reversed
// Consumer<Animal> is assignable to Consumer<Dog>
interface Consumer<in T> { set(arg: T): void; }
const consumeAnimal: Consumer<Animal> = { set: (a) => a.breathe() };
const consumeDog: Consumer<Dog> = consumeAnimal; // OK
// Why function params are contravariant (strictFunctionTypes)
// A handler that accepts Animal also works for Dog (Dog is-a Animal)
type Handler<T> = (event: T) => void;
// INVARIANCE β mutable containers
// Array<Dog> is NOT safely assignable to Array<Animal>
// (you could push a Cat into it)
// TypeScript 4.7 explicit variance annotations
interface Getter<out T> {
get(): T; // T only in output position β covariant
}
interface Setter<in T> {
set(value: T): void; // T only in input position β contravariant
}
interface ReadWrite<in out T> { // invariant
get(): T;
set(value: T): void;
}
// Variance in practice β function composition
function compose<A, B, C>(
f: (a: A) => B,
g: (b: B) => C
): (a: A) => C {
return (a) => g(f(a));
}Generic Type Inference β Contextual Typing and Template Literal Inference
TypeScript's type inference engine can infer generic type parameters from context, making explicit type annotations rare in well-typed code.
// Contextual typing β callback param types inferred from generic
const numbers: number[] = [3, 1, 2];
const sorted = numbers.sort((a, b) => a - b); // a and b are number
// Inference from multiple call sites
function mergeTwo<T>(a: T, b: T): T {
return { ...(a as object), ...(b as object) } as T;
}
// T inferred as { name: string } β best common type
const merged = mergeTwo({ name: 'Alice' }, { name: 'Bob' });
// infer in conditional types
type FirstParam<T> = T extends (first: infer F, ...rest: unknown[]) => unknown ? F : never;
type FP = FirstParam<(x: string, y: number) => void>; // string
// Template literal inference
type ParseRoute<T extends string> =
T extends `${infer Prefix}/${infer Param}`
? { prefix: Prefix; param: Param }
: { prefix: T; param: never };
type Route = ParseRoute<'/api/users'>; // { prefix: '/api'; param: 'users' }
// Inferring tuple types
type Head<T extends unknown[]> = T extends [infer H, ...unknown[]] ? H : never;
type Tail<T extends unknown[]> = T extends [unknown, ...infer R] ? R : never;
type Last<T extends unknown[]> = T extends [...unknown[], infer L] ? L : never;
type H = Head<[string, number, boolean]>; // string
type TailType = Tail<[string, number, boolean]>; // [number, boolean]
type L = Last<[string, number, boolean]>; // boolean
// Const assertion for narrowed literal inference
function createActions<const T extends readonly string[]>(actions: T): T {
return actions;
}
const myActions = createActions(['add', 'remove', 'update'] as const);
// readonly ["add", "remove", "update"] β not string[]Real-World Patterns β Typed API Client, Event Emitter, Builder, Type-Safe Forms
These patterns demonstrate how generics combine with other TypeScript features to solve real engineering problems with complete type safety.
Typed API Client
interface PostType { id: string; title: string; }
// Define endpoint schema
interface ApiSchema {
'/users': { GET: UserType[]; POST: UserType };
'/users/:id': { GET: UserType; PUT: UserType; DELETE: void };
'/posts': { GET: PostType[]; POST: PostType };
}
type Endpoint = keyof ApiSchema;
type HttpMethod<P extends Endpoint> = keyof ApiSchema[P] & string;
type ApiResponse<P extends Endpoint, M extends HttpMethod<P>> =
ApiSchema[P][M extends keyof ApiSchema[P] ? M : never];
async function apiRequest<P extends Endpoint, M extends HttpMethod<P>>(
path: P,
method: M,
body?: unknown
): Promise<ApiResponse<P, M>> {
const res = await fetch(path.toString(), {
method,
body: body ? JSON.stringify(body) : undefined,
headers: { 'Content-Type': 'application/json' },
});
return res.json() as Promise<ApiResponse<P, M>>;
}
// Usage
const users = await apiRequest('/users', 'GET'); // UserType[]
const oneUser = await apiRequest('/users/:id', 'GET'); // UserTypeTyped Event Emitter
interface AppEvents {
'user:login': { userId: string; timestamp: number };
'user:logout': { userId: string };
'post:created': { postId: string; authorId: string };
'error': { message: string; code: number };
}
class TypedEventEmitter<Events extends Record<string, unknown>> {
private listeners = new Map<keyof Events, Set<(data: unknown) => void>>();
on<K extends keyof Events>(event: K, handler: (data: Events[K]) => void): () => void {
if (!this.listeners.has(event)) this.listeners.set(event, new Set());
const fn = handler as (data: unknown) => void;
this.listeners.get(event)!.add(fn);
return () => this.listeners.get(event)!.delete(fn);
}
emit<K extends keyof Events>(event: K, data: Events[K]): void {
this.listeners.get(event)?.forEach(handler => handler(data));
}
}
const emitter = new TypedEventEmitter<AppEvents>();
emitter.on('user:login', ({ userId, timestamp }) => {
console.log(`User ${userId} logged in at ${timestamp}`);
});
emitter.emit('user:login', { userId: '123', timestamp: Date.now() });Builder Pattern with Type Tracking
// Type-safe query builder
interface QueryState {
table: string | null;
conditions: string[];
columns: string[];
limitVal: number | null;
}
class QueryBuilder<HasTable extends boolean = false> {
private state: QueryState = {
table: null, conditions: [], columns: [], limitVal: null
};
from(tableName: string): QueryBuilder<true> {
this.state.table = tableName;
return this as unknown as QueryBuilder<true>;
}
select(...cols: string[]): this {
this.state.columns = cols;
return this;
}
where(condition: string): this {
this.state.conditions.push(condition);
return this;
}
limit(n: number): this {
this.state.limitVal = n;
return this;
}
// Only callable when HasTable extends true
build(this: QueryBuilder<true>): string {
const cols = this.state.columns.length ? this.state.columns.join(', ') : '*';
const where = this.state.conditions.length
? ` WHERE ${this.state.conditions.join(' AND ')}`
: '';
const lim = this.state.limitVal !== null ? ` LIMIT ${this.state.limitVal}` : '';
return `SELECT ${cols} FROM ${this.state.table}${where}${lim}`;
}
}
const query = new QueryBuilder()
.from('users')
.select('id', 'name')
.where('age > 18')
.limit(10)
.build(); // "SELECT id, name FROM users WHERE age > 18 LIMIT 10"Type-Safe Form Handling with Zod Inference
// Without Zod β manual approach
interface LoginForm {
email: string;
password: string;
remember: boolean;
}
// Generic form validator
type ValidationErrors<T> = Partial<Record<keyof T, string>>;
function validateForm<T extends Record<string, unknown>>(
data: unknown,
rules: { [K in keyof T]: (val: unknown) => string | null }
): { valid: true; data: T } | { valid: false; errors: ValidationErrors<T> } {
const errors: ValidationErrors<T> = {};
const result = {} as T;
for (const key in rules) {
const val = (data as Record<string, unknown>)[key];
const error = rules[key](val);
if (error) {
errors[key] = error;
} else {
result[key] = val as T[typeof key];
}
}
const hasErrors = Object.keys(errors).length > 0;
return hasErrors
? { valid: false, errors }
: { valid: true, data: result };
}
// Usage
const result = validateForm<LoginForm>(
{ email: 'alice@example.com', password: 'hunter2!' },
{
email: (v) => typeof v === 'string' && v.includes('@') ? null : 'Invalid email',
password: (v) => typeof v === 'string' && v.length >= 8 ? null : 'Min 8 chars',
remember: () => null,
}
);
if (result.valid) {
console.log(result.data.email); // string β fully typed
}TypeScript Built-in Utility Types β Quick Reference
All of these are defined in TypeScript's lib.es5.d.ts using the generic type features covered in this guide.
| Utility Type | Transforms | Example |
|---|---|---|
Partial<T> | All properties optional | Partial<User> β all fields ? |
Required<T> | All properties required | Removes all ? modifiers |
Readonly<T> | All properties readonly | Prevents mutation after creation |
Record<K, V> | Map K keys to V values | Record<string, number> |
Pick<T, K> | Keep only keys K from T | Pick<User, "id" | "name"> |
Omit<T, K> | Remove keys K from T | Omit<User, "password"> |
Extract<T, U> | Keep union members in U | Extract<"a"|"b"|"c", "a"|"c"> β "a"|"c" |
Exclude<T, U> | Remove union members in U | Exclude<string|null, null> β string |
NonNullable<T> | Remove null and undefined | NonNullable<string | null> β string |
ReturnType<T> | Return type of function T | ReturnType<typeof fetch> |
Parameters<T> | Tuple of function param types | Parameters<typeof console.log> |
Awaited<T> | Unwrap nested Promises | Awaited<Promise<Promise<number>>> β number |
Recursive Generic Types β JSON, Tree, and Deep Transformations
TypeScript supports recursive type aliases, enabling you to describe types like JSON values, nested trees, and deeply nested transformations.
// Type-safe JSON
type JSONPrimitive = string | number | boolean | null;
type JSONObject = { [key: string]: JSONValue };
type JSONArray = JSONValue[];
type JSONValue = JSONPrimitive | JSONObject | JSONArray;
const json: JSONValue = {
name: 'Alice',
scores: [100, 200, 300],
meta: { active: true, tags: ['admin', 'user'] },
};
// Binary tree
interface TreeNode<T> {
value: T;
left: TreeNode<T> | null;
right: TreeNode<T> | null;
}
function treeMap<T, U>(node: TreeNode<T> | null, fn: (val: T) => U): TreeNode<U> | null {
if (!node) return null;
return {
value: fn(node.value),
left: treeMap(node.left, fn),
right: treeMap(node.right, fn),
};
}
// Deep readonly β makes all nested properties readonly
type DeepReadonly<T> =
T extends (infer U)[]
? ReadonlyArray<DeepReadonly<U>>
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
interface Config {
server: { host: string; port: number };
db: { url: string; pool: { min: number; max: number } };
}
type ImmutableConfig = DeepReadonly<Config>;
// All nested fields are readonly β even deeply nested port and min/max
// Deep flatten β flatten all nested arrays
type Flatten<T> = T extends (infer U)[] ? Flatten<U> : T;
type F = Flatten<number[][][]>; // numberDiscriminated Unions with Generics β Result and Option Types
Combining discriminated unions with generics produces powerful, type-safe result types that eliminate runtime errors by making error handling mandatory at the type level.
// Result<T, E> β explicit success or failure
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function divide(a: number, b: number): Result<number, string> {
if (b === 0) return { ok: false, error: 'Division by zero' };
return { ok: true, value: a / b };
}
const r = divide(10, 2);
if (r.ok) {
console.log(r.value); // number β TypeScript knows this is the success case
} else {
console.error(r.error); // string
}
// Option<T> β nullable without null (like Rust's Option<T>)
type Option<T> =
| { some: true; value: T }
| { some: false };
const none: Option<never> = { some: false };
function some<T>(value: T): Option<T> { return { some: true, value }; }
function findUser(id: string): Option<UserType> {
const users: UserType[] = [{ id: '1', name: 'Alice', email: 'a@b.com' }];
const user = users.find(u => u.id === id);
return user ? some(user) : none;
}
const maybeUser = findUser('1');
if (maybeUser.some) {
console.log(maybeUser.value.name); // TypeScript narrows to { some: true; value: UserType }
}
// Generic Result helpers
function mapResult<T, U, E>(result: Result<T, E>, fn: (val: T) => U): Result<U, E> {
return result.ok ? { ok: true, value: fn(result.value) } : result;
}
function flatMapResult<T, U, E>(
result: Result<T, E>,
fn: (val: T) => Result<U, E>
): Result<U, E> {
return result.ok ? fn(result.value) : result;
}tsconfig Settings That Affect Generic Behavior
Several TypeScript compiler options change how generics are checked and inferred. Understanding these settings helps you get the most out of the type system.
| Option | Effect on Generics |
|---|---|
strict | Enables strictFunctionTypes, strictNullChecks β essential for correct variance and null-safety in generics |
strictFunctionTypes | Enforces contravariance for function parameters β prevents unsafe function type substitution |
noImplicitAny | Forces explicit generic type arguments when inference would resolve to any |
useUnknownInCatchVariables | Makes catch clause error type unknown instead of any β safer for generic error types |
exactOptionalPropertyTypes | Distinguishes undefined from missing key in mapped types with optional properties |
Common Generic Pitfalls and How to Avoid Them
// β PITFALL 1: Using 'any' instead of generics
function badIdentity(arg: any): any { return arg; }
const r1 = badIdentity('hello'); // r1 is 'any' β type info lost
// β
FIX: Use a generic
function goodIdentity<T>(arg: T): T { return arg; }
const r2 = goodIdentity('hello'); // r2 is 'string' β preserved
// β PITFALL 2: Over-constraining with specific types
function processItems(items: string[]): string[] {
return items.map(i => i.toUpperCase());
}
// This ONLY works for strings β not reusable
// β
FIX: Use generics with minimal constraints
function transformItems<T>(items: T[], transform: (item: T) => T): T[] {
return items.map(transform);
}
// β PITFALL 3: Forgetting distributive behavior of conditional types
type Wrap<T> = T extends unknown ? { value: T } : never;
type Result = Wrap<string | number>;
// Result = { value: string } | { value: number } (distributed!)
// NOT { value: string | number }
// β
FIX: Wrap in tuple to prevent distribution
type WrapUnion<T> = [T] extends [unknown] ? { value: T } : never;
type Result2 = WrapUnion<string | number>;
// { value: string | number }
// β PITFALL 4: Using generic type parameter in wrong position
interface BadContainer<T> {
items: T[];
addItem(item: string): void; // T not used β should be T
}
// β
FIX: Use the type parameter consistently
interface GoodContainer<T> {
items: T[];
addItem(item: T): void;
}
// β PITFALL 5: Calling .toUpperCase() without a string constraint
function upper<T>(s: T): string {
return (s as unknown as string).toUpperCase(); // unsafe cast
}
// β
FIX: Constrain T to string
function upperStr<T extends string>(s: T): Uppercase<T> {
return s.toUpperCase() as Uppercase<T>;
}Generic Higher-Order Functions β memoize, retry, and debounce
Higher-order functions (functions that take or return functions) are a natural fit for generics because they need to preserve the type signatures of the functions they wrap.
// Typed memoize β preserves the function signature exactly
function memoize<T extends (...args: unknown[]) => unknown>(fn: T): T {
const cache = new Map<string, ReturnType<T>>();
return ((...args: Parameters<T>): ReturnType<T> => {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key)!;
const result = fn(...args) as ReturnType<T>;
cache.set(key, result);
return result;
}) as T;
}
function expensiveCalc(n: number): number { return n * n; }
const memoCalc = memoize(expensiveCalc);
memoCalc(5); // number β return type preserved
// Typed retry β retries an async function N times on failure
async function retry<T>(
fn: () => Promise<T>,
retries: number,
delayMs = 1000
): Promise<T> {
try {
return await fn();
} catch (err) {
if (retries <= 0) throw err;
await new Promise(resolve => setTimeout(resolve, delayMs));
return retry(fn, retries - 1, delayMs * 2);
}
}
const data = await retry(() => fetch('/api/data').then(r => r.json()), 3);
// data is 'any' from fetch β but works with typed fetch wrappers too
// Typed debounce β preserves parameter types
function debounce<T extends (...args: unknown[]) => void>(
fn: T,
waitMs: number
): (...args: Parameters<T>) => void {
let timer: ReturnType<typeof setTimeout>;
return (...args: Parameters<T>) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), waitMs);
};
}
function onSearch(query: string, page: number): void {
console.log(`Searching: ${query} page ${page}`);
}
const debouncedSearch = debounce(onSearch, 300);
debouncedSearch('hello', 1); // (query: string, page: number) => void β typed!
// Typed once β run function at most once
function once<T extends (...args: unknown[]) => unknown>(fn: T): T {
let called = false;
let result: ReturnType<T>;
return ((...args: Parameters<T>): ReturnType<T> => {
if (!called) {
called = true;
result = fn(...args) as ReturnType<T>;
}
return result;
}) as T;
}
const initApp = once((port: number) => {
console.log(`Starting on port ${port}`);
return { port };
});
initApp(3000); // runs β returns { port: number }
initApp(4000); // no-op β returns cached resultGeneric Middleware Pipeline β Type-Safe Composition
// Type-safe pipe β compose functions left to right
type Pipe<T extends unknown[], R> =
T extends [infer First, ...infer Rest]
? First extends (arg: infer A) => infer B
? Rest extends [(arg: B) => unknown, ...unknown[]]
? Pipe<Rest, R>
: never
: never
: R;
// Simple runtime pipe for two-step example
function pipe2<A, B, C>(
fn1: (a: A) => B,
fn2: (b: B) => C
): (a: A) => C {
return (a) => fn2(fn1(a));
}
const double = (n: number): number => n * 2;
const stringify = (n: number): string => String(n);
const trim = (s: string): string => s.trim();
const doubleThenStringify = pipe2(double, stringify);
const result = doubleThenStringify(21); // "42" β string
// Middleware context pattern
interface Context<T extends object> {
data: T;
meta: Record<string, unknown>;
}
type Middleware<T extends object, U extends object> =
(ctx: Context<T>, next: (ctx: Context<U>) => void) => void;
function composeMiddleware<T extends object>(
middlewares: Middleware<T, T>[]
): (ctx: Context<T>) => void {
return (ctx) => {
let index = -1;
function dispatch(i: number, c: Context<T>): void {
if (i <= index) throw new Error('next() called multiple times');
index = i;
const fn = middlewares[i];
if (!fn) return;
fn(c, (next) => dispatch(i + 1, next as Context<T>));
}
dispatch(0, ctx);
};
}Quick Tool: Convert TypeScript to JavaScript instantly with our TypeScript to JavaScript converter β paste your generic TypeScript code and see the compiled output in real time, online and free.
Key Takeaways
- Generics are type-level functions:
<T>is a parameter that captures a type at the call site through inference β explicit annotation is rarely needed. - Constraints narrow valid types: use
T extends SomeTypeto require properties; useK extends keyof Tfor type-safe property access. - Conditional types compute types:
T extends U ? X : Yis type-level if/else;inferextracts types from patterns like return types and array elements. - Mapped types transform shapes: iterate over
keyof Twith modifier changes; useasclause (TS 4.1+) for key remapping and filtering. - Template literal types type strings: union distribution produces all permutations, enabling typed route params, event names, and CSS properties.
- Learn the built-in utility types: Awaited, Parameters, ReturnType, ConstructorParameters, InstanceType, OmitThisParameter, Extract, Exclude.
- Function parameters are contravariant: a function accepting a base type can substitute for one accepting a subtype with
--strictFunctionTypes. - Use
in/outvariance annotations (TS 4.7) for generic interfaces to document intent and enable compiler variance checks. - Zod +
z.infereliminates duplicate type definitions: define your schema once and derive both runtime validation and compile-time types from it. - Real patterns are composable: typed event emitters, API clients, and builders all combine generics + constraints + conditional types into reusable, type-safe abstractions.
- Enable
strictmode:strictFunctionTypesenforces contravariance for function parameters, catching subtle subtype-safety bugs in generic code. - Recursive generics model nested data: use recursive type aliases for JSON, trees, and deep transformations like
DeepPartial<T>andDeepReadonly<T>. - Higher-order function wrappers preserve types: use
Parameters<T>andReturnType<T>to preserve the wrapped function's full signature in memoize, debounce, and retry utilities. - Result<T, E> makes error handling explicit: discriminated union result types force callers to handle both success and failure cases at the type level, eliminating forgotten catch blocks.