TypeScript Generics Explained: A Practical Guide with Real Examples
Generics are one of the most powerful features in TypeScript β and one of the most avoided by developers who find them intimidating. Once you understand them, you will use them constantly. They are the difference between type-safe reusable code and copy-pasted functions with slightly different types.
This guide builds your understanding from first principles to advanced patterns.
Why Generics Exist
Imagine you need a function that returns the first element of an array. Without generics:
typescriptfunction firstString(arr: string[]): string { return arr[0]; } function firstNumber(arr: number[]): number { return arr[0]; }
That is repetition. You could use any:
typescriptfunction first(arr: any[]): any { return arr[0]; }
But now you have lost all type information. If you call first(["hello", "world"]), TypeScript has no idea the return type is string.
Generics solve both problems β one function, full type safety:
typescriptfunction first<T>(arr: T[]): T { return arr[0]; } const s = first(["hello", "world"]); // inferred: string const n = first([1, 2, 3]); // inferred: number
T is a type parameter β a placeholder TypeScript replaces with the actual type at the call site.
Basic Syntax
The angle brackets <T> declare a type parameter. You can use any name, but single letters like T, K, V are convention:
typescriptfunction identity<T>(value: T): T { return value; } function pair<A, B>(a: A, b: B): [A, B] { return [a, b]; } const result = pair("hello", 42); // [string, number]
TypeScript infers the type arguments from what you pass in. You can also be explicit:
typescriptconst result = pair<string, number>("hello", 42);
Generic Interfaces and Types
Generics are not just for functions β they work on interfaces and type aliases too.
typescriptinterface ApiResponse<T> { data: T; status: number; message: string; } interface User { id: number; name: string; } const response: ApiResponse<User> = { data: { id: 1, name: "Alice" }, status: 200, message: "OK", }; const listResponse: ApiResponse<User[]> = { data: [{ id: 1, name: "Alice" }], status: 200, message: "OK", };
This pattern is everywhere in real codebases β API response wrappers, paginated results, event payloads.
Generic Classes
typescriptclass 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]; } get size(): number { return this.items.length; } } const numberStack = new Stack<number>(); numberStack.push(1); numberStack.push(2); numberStack.pop(); // 2 const stringStack = new Stack<string>(); stringStack.push("hello");
Generic Constraints
Sometimes you need to restrict what types T can be. Use extends to add constraints:
typescriptfunction getLength<T extends { length: number }>(value: T): number { return value.length; } getLength("hello"); // 5 getLength([1, 2, 3]); // 3 getLength({ length: 10, name: "file" }); // 10 getLength(42); // Error: number has no length property
Constraining to object keys
A very common pattern β ensuring a key actually exists on an object:
typescriptfunction getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } const user = { id: 1, name: "Alice", age: 30 }; getProperty(user, "name"); // string getProperty(user, "age"); // number getProperty(user, "role"); // Error: "role" not in keyof user
keyof T produces a union of all keys of T. K extends keyof T means K must be one of those keys.
Default Type Parameters
Like default function parameters, type parameters can have defaults:
typescriptinterface Pagination<T = unknown> { items: T[]; page: number; total: number; } const untyped: Pagination = { items: [], page: 1, total: 0 }; const typed: Pagination<User> = { items: [], page: 1, total: 0 };
Built-in Utility Types
TypeScript ships with generic utility types that you will use constantly:
Partial and Required
typescriptinterface User { id: number; name: string; email: string; } type PartialUser = Partial<User>; -- All fields become optional -- { id?: number; name?: string; email?: string } type RequiredUser = Required<Partial<User>>; -- All fields become required again
Pick and Omit
typescripttype UserPreview = Pick<User, "id" | "name">; -- { id: number; name: string } type UserWithoutId = Omit<User, "id">; -- { name: string; email: string }
Record
typescripttype RolePermissions = Record<string, boolean>; const permissions: RolePermissions = { canRead: true, canWrite: false, canDelete: false, }; type Status = "pending" | "active" | "inactive"; type StatusLabels = Record<Status, string>; const labels: StatusLabels = { pending: "Awaiting approval", active: "Active account", inactive: "Account disabled", };
ReturnType and Parameters
typescriptfunction fetchUser(id: number, includeProfile: boolean) { return { id, name: "Alice" }; } type FetchUserReturn = ReturnType<typeof fetchUser>; -- { id: number; name: string } type FetchUserParams = Parameters<typeof fetchUser>; -- [id: number, includeProfile: boolean]
Conditional Types
Advanced generics can branch based on type conditions:
typescripttype IsArray<T> = T extends any[] ? true : false; type A = IsArray<string[]>; -- true type B = IsArray<string>; -- false -- Extract the element type from an array type ElementType<T> = T extends (infer U)[] ? U : never; type StrElement = ElementType<string[]>; -- string type NumElement = ElementType<number[]>; -- number
Conditional types with
inferlook complex but appear in many library types. The patternT extends SomeType<infer U> ? U : nevermeans "if T matches this shape, extract U from it."
Real-World Example: A Typed fetch Wrapper
Generics shine when building utility functions used across a codebase:
typescriptasync function apiFetch<T>(url: string): Promise<T> { const res = await fetch(url); if (!res.ok) { throw new Error(`HTTP ${res.status}: ${url}`); } return res.json() as Promise<T>; } interface Product { id: number; name: string; price: number; } const product = await apiFetch<Product>("/api/products/1"); -- product is fully typed as Product
Common Interview Questions
Q: What is the difference between any and generics?
any disables type checking entirely. Generics preserve type information β TypeScript tracks the actual type and enforces it throughout. Use generics when you want flexibility without sacrificing safety.
Q: What does T extends keyof U mean?
It constrains T to be one of the keys of type U. This ensures you cannot pass a key that does not exist on the object.
Q: What is the difference between interface Foo<T> and type Foo<T>?
Both support generics. interface supports declaration merging and is slightly preferred for public API shapes. type is more flexible β it can represent unions, intersections, and mapped types. In practice, either works for most generic shapes.
Practice TypeScript on Froquiz
TypeScript is increasingly required in frontend and backend roles. Test your skills on Froquiz across a wide range of JavaScript and TypeScript topics.
Summary
- Generics use type parameters (
<T>) to write reusable, type-safe code - Use
extendsto constrain what types are allowed keyof TandK extends keyof Tenable type-safe property access- Built-in utility types (
Partial,Pick,Omit,Record,ReturnType) use generics under the hood - Conditional types with
inferlet you extract types programmatically - Prefer generics over
anywhenever you need flexibility without losing type safety