Chapter 15: TypeScript Basic Generics
TypeScript Basic Generics (aimed at someone who already knows basic types + interfaces + functions)
Imagine we’re sitting together — I’m drawing on a whiteboard, you’re asking questions whenever something feels unclear.
1. The core idea of generics — the one-sentence version
Generics let you write code that works with many different types while still keeping strong type safety.
Without generics you either:
- write the same logic many times (once for string, once for number, once for User, …)
- or use any / unknown → lose type safety
With generics you write the logic once and let the caller decide what concrete type should be used.
2. The simplest realistic example everyone meets first
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// Without generics (bad style — repetition) function wrapInArray_string(value: string): string[] { return [value]; } function wrapInArray_number(value: number): number[] { return [value]; } function wrapInArray_user(value: User): User[] { return [value]; } |
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
// With basic generics (clean + type-safe) function wrapInArray<T>(value: T): T[] { return [value]; } const names = wrapInArray("alice"); // string[] const scores = wrapInArray(92); // number[] const products = wrapInArray({ id: 1, name: "Laptop" }); // {id:number, name:string}[] |
Key moment: When you write <T>, you are declaring a type parameter (like a variable, but for types). The name T is just convention — you can write <Item>, <Element>, <U>, etc.
3. Where do we usually see the <T> ?
| Place | Syntax example | Meaning / When used |
|---|---|---|
| Function | function identity<T>(arg: T): T | Most common starting point |
| Interface | interface Box<T> { content: T } | Very frequent — reusable data shapes |
| Class | class Queue<T> { … } | Data structures, state holders |
| Type Alias | type Pair<K, V> = { key: K; value: V } | Less common than interface, still useful |
| Arrow function | const wrap = <T>(x: T) => [x] | Works, but has JSX parsing gotcha (later) |
4. Classic real-world examples (copy-paste friendly)
4.1 Identity function (the hello-world of generics)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
function identity<T>(value: T): T { return value; } const a = identity(42); // number const b = identity("hello"); // string const c = identity({ x: 10 }); // { x: number } |
4.2 First element of array (very common utility)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
function first<T>(arr: T[]): T | undefined { return arr[0]; } const firstName = first(["alice", "bob"]); // string | undefined const firstScore = first([88, 91, 76]); // number | undefined // first(123) // Error — not array |
4.3 Generic interface — very frequent pattern
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
interface ApiResponse<T> { success: boolean; data?: T; error?: string; metadata: { timestamp: Date; requestId: string; }; } const userResponse: ApiResponse<User> = { success: true, data: { id: 1, name: "Sara" }, metadata: { timestamp: new Date(), requestId: "abc123" } }; const productsResponse: ApiResponse<Product[]> = { ... }; |
4.4 Generic class — Queue / Stack example
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
class Queue<T> { private items: T[] = []; enqueue(item: T): void { this.items.push(item); } dequeue(): T | undefined { return this.items.shift(); } peek(): T | undefined { return this.items[0]; } get size(): number { return this.items.length; } } const numberQueue = new Queue<number>(); numberQueue.enqueue(10); numberQueue.enqueue(20); // numberQueue.enqueue("hello"); // Error — string not allowed const stringQueue = new Queue<string>(); stringQueue.enqueue("task-1"); |
5. Multiple type parameters (very common after 2–3 months)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function merge<T, U>(obj1: T, obj2: U): T & U { return { ...obj1, ...obj2 }; } const result = merge( { name: "John", age: 30 }, { city: "Berlin", active: true } ); // result → { name: string; age: number; city: string; active: boolean } |
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
type KeyValuePair<K, V> = { key: K; value: V; }; const pair1: KeyValuePair<string, number> = { key: "count", value: 42 }; const pair2: KeyValuePair<number, User> = { key: 1001, value: { name: "Emma" } }; |
6. Very important: Default type parameters
|
0 1 2 3 4 5 6 7 8 9 10 11 |
interface Container<T = string> { value: T; } const c1: Container = { value: "hello" }; // T = string (default) const c2: Container<number> = { value: 999 }; // override default |
Very common in:
- React hooks (useState<T>(initial: T))
- HTTP clients (axios.get<T>(url))
7. Most common “beginner confusion” points (2026 edition)
7.1 Arrow functions + JSX parsing problem
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
// ❌ This breaks in .tsx files const wrap = <T>(item: T) => [item]; // ✅ Two popular fixes const wrap = <T,>(item: T) => [item]; // comma trick const wrap = <T>(item: T) => [item] as T[]; // assertion (ugly but works) function wrap<T>(item: T) { return [item]; } // classic function — safest |
7.2 When TypeScript cannot infer → you must write <Type>
|
0 1 2 3 4 5 6 7 8 9 |
const divs = Array(10).fill(null).map(() => <div>Hello</div>); // JSX → any[] // ↑ inference fails here const divs = Array<HTMLDivElement>(10).fill(null).map(() => <div>Hello</div>); |
7.3 const assertions + generics (powerful combo)
|
0 1 2 3 4 5 6 7 8 9 10 11 |
function asTuple<T extends readonly any[]>(arr: T): T { return arr; } const colors = asTuple(["red", "green", "blue"] as const); // colors → readonly ["red", "green", "blue"] |
Quick reference card — basic generics patterns
| Pattern | Code example | Typical usage area |
|---|---|---|
| Generic function | function getFirst<T>(arr: T[]): T | Utils, helpers |
| Generic interface | interface Result<T> { data: T } | API responses, form values |
| Generic class | class List<T> { add(item: T) } | Collections, state, caches |
| Multiple parameters | function zip<A,B>(a:A[], b:B[]): [A,B][] | Combining different types |
| Default type | type Box<T=string> = { item: T } | Optional generic arguments |
| Generic constraint | function longest<T extends { length: number }> | Restrict what T can be (very important — next topic) |
Next logical steps after basic generics usually are:
- Constraints (extends)
- keyof, indexed access types
- Generic functions with extends + keyof
- React hooks (useState, useReducer, custom hooks)
Would you like to continue with one of these next?
Or would you prefer:
- 10 more small realistic examples right now
- Explanation of constraints (<T extends …>) with examples
- Common generic utility types from lib (Partial<T>, Pick<T,K>, Record<K,V>, …)
- Generic React component patterns
Tell me what feels most useful for you at this moment 😄
