TypeScript Generics | Complete Guide
이 글의 핵심
Generics in TypeScript: typed identity functions, generic functions and classes, constraints with extends and keyof, caches, and common mistakes—tutorial for reusable safe APIs.
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
Related posts
- C++ templates basics
- [Advanced TypeScript types](/en/blog/typescript-series-02-types/
- [TypeScript interfaces | Complete guide](/en/blog/typescript-series-03-interface/
- C++ numeric_limits
- C++ union and std::variant
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Generics in TypeScript: typed identity functions, generic functions and classes, constraints with extends and keyof, cac… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- [TypeScript Interfaces | Complete Guide](/en/blog/typescript-series-03-interface/
- TypeScript 유틸리티 타입 | Partial, Pick, Omit, Record
- [Advanced TypeScript Types | Union· Intersection](/en/blog/typescript-series-02-types/
이 글에서 다루는 키워드 (관련 검색어)
TypeScript, Generics, Type Parameters, Constraints, keyof 등으로 검색하시면 이 글이 도움이 됩니다.