TypeScript Generics | Complete Guide
이 글의 핵심
A practical guide to TypeScript generics: replacing any with type parameters, functions and classes, constraints, keyof, and patterns like caches and async wrappers.
Introduction
What are generics?
Generics let you treat types like parameters so you can build reusable, type-safe components.
1. Generics basics
The problem
If a function must accept many types, using any throws away safety:
// any — loses type safety
function identity(value: any): any {
return value;
}
const result1 = identity("hello"); // any
const result2 = identity(123); // any
// Issues:
// 1. Return type is any — no checking
// 2. result1.toFixed() might compile but fail at runtime
// 3. Input/output relationship is not expressed
The generic solution
// <T> declares a type parameter (T is conventional)
function identity<T>(value: T): T {
return value;
}
const result1 = identity<string>("hello");
const result2 = identity<number>(123);
// Inference (preferred)
const result3 = identity("hello"); // T inferred as string
const result4 = identity(123); // T inferred as number
// Now the compiler preserves accuracy
// result3.toUpperCase(); // ✅
// result3.toFixed(); // ❌ error
// result4.toFixed(2); // ✅
Benefits:
- Safety: checked at compile time
- Reuse: one implementation for many types
- Clarity: documents type relationships
- Tooling: better autocomplete
Generics vs any
| Aspect | Generics (<T>) | any |
|---|---|---|
| Safety | ✅ | ❌ |
| Inference | ✅ | ❌ |
| Autocomplete | ✅ | ❌ |
| Runtime surprises | ✅ reduced | ❌ likely |
2. Generic functions
Basics
function getFirstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
const numbers = [1, 2, 3];
const first = getFirstElement(numbers);
const strings = ["a", "b", "c"];
const firstStr = getFirstElement(strings);
Multiple type parameters
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const result1 = pair("hello", 123);
const result2 = pair(true, "world");
Arrow functions
const map = <T, U>(arr: T[], fn: (item: T) => U): U[] => {
return arr.map(fn);
};
const numbers = [1, 2, 3];
const doubled = map(numbers, (n) => n * 2);
const strings = map(numbers, (n) => n.toString());
3. Generic interfaces
Basics
interface Box<T> {
value: T;
}
const numberBox: Box<number> = { value: 123 };
const stringBox: Box<string> = { value: "hello" };
const boxOfBoxes: Box<Box<number>> = {
value: { value: 123 }
};
Example: API responses
interface ApiResponse<T> {
success: boolean;
data: T;
error?: string;
}
interface User {
id: string;
name: string;
email: string;
}
interface Product {
id: string;
name: string;
price: number;
}
const userResponse: ApiResponse<User> = {
success: true,
data: {
id: "U001",
name: "Alice",
email: "[email protected]"
}
};
const productResponse: ApiResponse<Product[]> = {
success: true,
data: [
{ id: "P001", name: "Laptop", price: 1000000 },
{ id: "P002", name: "Mouse", price: 30000 }
]
};
4. Generic classes
Basics
Generic classes are ideal for reusable data structures:
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 numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.push(3);
console.log(numberStack.pop());
console.log(numberStack.peek());
// numberStack.push("hello"); // ❌ error
const stringStack = new Stack<string>();
stringStack.push("hello");
stringStack.push("world");
console.log(stringStack.pop());
// stringStack.push(123); // ❌ error
Why generic classes help:
- One class for many element types
- No duplicate
StackNumber,StackString, etc. - Prevents mixing wrong types in the same stack
const taskStack = new Stack<Task>();
const undoStack = new Stack<Action>();
const historyStack = new Stack<string>();
5. Generic constraints
extends
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(value: T): void {
console.log(value.length);
}
logLength("hello"); // ✅
logLength([1, 2, 3]); // ✅
// logLength(123); // ❌ number has no length
keyof
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = {
name: "Alice",
age: 25,
email: "[email protected]"
};
const name = getProperty(user, "name");
const age = getProperty(user, "age");
// const invalid = getProperty(user, "invalid"); // ❌ error
6. Hands-on examples
Example 1: Chunk arrays
function chunk<T>(arr: T[], size: number): T[][] {
const result: T[][] = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
}
const numbers = [1, 2, 3, 4, 5, 6];
console.log(chunk(numbers, 2));
const strings = ["a", "b", "c", "d"];
console.log(chunk(strings, 3));
Example 2: Key–value cache
class Cache<K, V> {
private store = new Map<K, V>();
set(key: K, value: V): void {
this.store.set(key, value);
}
get(key: K): V | undefined {
return this.store.get(key);
}
has(key: K): boolean {
return this.store.has(key);
}
delete(key: K): boolean {
return this.store.delete(key);
}
clear(): void {
this.store.clear();
}
}
const userCache = new Cache<string, User>();
userCache.set("U001", { id: "U001", name: "Alice", email: "[email protected]" });
const user = userCache.get("U001");
console.log(user?.name);
Example 3: Promise wrapper
class AsyncResult<T> {
constructor(private promise: Promise<T>) {}
async map<U>(fn: (value: T) => U): Promise<AsyncResult<U>> {
const value = await this.promise;
return new AsyncResult(Promise.resolve(fn(value)));
}
async flatMap<U>(fn: (value: T) => Promise<U>): Promise<AsyncResult<U>> {
const value = await this.promise;
return new AsyncResult(fn(value));
}
async unwrap(): Promise<T> {
return await this.promise;
}
}
const result = new AsyncResult(Promise.resolve(10));
result
.map((x) => x * 2)
.then((r) => r.unwrap())
.then((value) => console.log(value)); // 20
7. Advanced patterns
Conditional types
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
Mapped types (preview)
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
interface User {
name: string;
age: number;
}
type ReadonlyUser = Readonly<User>;
// { readonly name: string; readonly age: number; }
8. Common mistakes
Mistake 1: Accessing properties without a constraint
// ❌ Wrong
function getLength<T>(value: T): number {
return value.length; // T might not have length
}
// ✅ Correct
function getLength<T extends { length: number }>(value: T): number {
return value.length;
}
Mistake 2: Unnecessary generics
// ❌ Generic adds nothing
function log<T>(message: string): void {
console.log(message);
}
// ✅ Simpler
function log(message: string): void {
console.log(message);
}
Summary
Takeaways
- Generics: parameterize types
- Functions:
function fn<T>(value: T): T - Interfaces:
interface Box<T> - Classes:
class Stack<T> - Constraints:
<T extends SomeType> - keyof: keys of object types
Next steps
- TypeScript utility types
- TypeScript decorators
- Advanced TypeScript patterns
Related posts
- C++ templates basics
- Advanced TypeScript types
- TypeScript interfaces | Complete guide
- C++ numeric_limits
- C++ union and std::variant