// LOADING
// LOADING
// LOADING_ARTICLE
Generics are the point where TypeScript stops being "JavaScript with type annotations" and becomes a real type system. They let you write functions and data structures that work across many types while still being fully type-safe. If you have copied <T> from documentation without fully understanding it, this post fixes that.
Without generics, you face a choice between two bad options when writing reusable code:
any, which throws away type safety entirely.Consider a simple identity function:
ts// Bad: loses all type information function identity(value: any): any { return value; } const result = identity(42); // type is `any`, not `number`
Generics give you a third option: the type is unknown at definition time, but the compiler captures it at call time.
tsfunction identity<T>(value: T): T { return value; } const result = identity(42); // type is inferred as `number` const name = identity('Alice'); // type is inferred as `string`
T is a type parameter — a placeholder that gets filled in when you call the function. The compiler infers T from the argument you pass, so you rarely need to write identity<number>(42) explicitly.
The classic real-world case is a function that wraps an array operation:
tsfunction first<T>(arr: T[]): T | undefined { return arr[0]; } const firstNumber = first([1, 2, 3]); // number | undefined const firstString = first(['a', 'b']); // string | undefined
The return type is derived from the input type. No casting, no any.
Multiple type parameters work the same way:
tsfunction pair<A, B>(a: A, b: B): [A, B] { return [a, b]; } const p = pair('id', 42); // [string, number]
Generics are not limited to functions. Interfaces and type aliases use them too:
tsinterface ApiResponse<T> { data: T; status: number; error: string | null; } type UserResponse = ApiResponse<{ id: string; name: string }>; type ListResponse<T> = ApiResponse<T[]>;
This pattern is central to typing API responses end to end. You define ApiResponse<T> once and reuse it across your entire client/server boundary.
Sometimes you need to know something about T to operate on it. The extends keyword constrains a type parameter:
tsfunction getLength<T extends { length: number }>(value: T): number { return value.length; } getLength('hello'); // 5 getLength([1, 2, 3]); // 3 getLength(42); // Error: number does not have a `length` property
T extends { length: number } means: T can be any type, as long as it has a length property of type number. Strings, arrays, and custom objects all qualify.
A more practical example — fetching an entity by ID:
tsinterface HasId { id: string; } async function fetchById<T extends HasId>( url: string, id: string ): Promise<T> { const res = await fetch(`${url}/${id}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json() as Promise<T>; } // TypeScript knows the return is a User with id, name, email const user = await fetchById<User>('/api/users', '123');
keyof Constraint PatternOne of the most useful generic patterns is constraining a parameter to be a key of another type:
tsfunction getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } const user = { id: '1', name: 'Alice', age: 30 }; const name = getProperty(user, 'name'); // type: string const age = getProperty(user, 'age'); // type: number getProperty(user, 'email'); // Error: not a key of user
T[K] is an indexed access type — the type you get by looking up key K on object T. This prevents you from accessing properties that do not exist and preserves the exact return type.
Classes use the same syntax:
tsclass 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]; } } const stack = new Stack<number>(); stack.push(1); stack.push(2); const top = stack.pop(); // number | undefined
TypeScript 2.3 added default type parameters — useful when a generic has a sensible fallback:
tsinterface PaginatedResult<T = unknown> { items: T[]; total: number; page: number; } // Without explicit T, items is unknown[] const result: PaginatedResult = { items: [], total: 0, page: 1 }; // With explicit T, items is User[] const users: PaginatedResult<User> = { items: [], total: 0, page: 1 };
Generics add complexity. Do not use them when:
<T>(x: T): T but never actually use T inside the function body in a meaningful way.string | number is clearer for a fixed set of options.If you find yourself writing deeply nested generic constraints, consider whether a discriminated union would model the problem more clearly.
useState<User | null>(null), useRef<HTMLInputElement>(null)fetch<T>() returning Promise<T>useForm<FormValues>()z.object({}) returns a ZodObject<...> generic type, which feeds directly into runtime validation with ZodGenerics are the mechanism TypeScript uses to express "this works for many types, and I will tell you which type it is when I use it." The core ideas — type parameters, constraints with extends, and keyof — cover the vast majority of real-world usage. Start with simple functions returning T, add constraints when you need to access properties, and you will find generics become intuitive quickly. For broader context on why the TypeScript type system matters in the first place, see why type safety is becoming essential in modern JavaScript development. If you want to see generics applied across a full stack, browse the projects built with TypeScript throughout.