본문으로 건너뛰기
Previous
Next
TypeScript Generics | Complete Guide

TypeScript Generics | Complete Guide

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:

  1. Safety: checked at compile time
  2. Reuse: one implementation for many types
  3. Clarity: documents type relationships
  4. 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

  1. Generics: parameterize types
  2. Functions: function fn<T>(value: T): T
  3. Interfaces: interface Box<T>
  4. Classes: class Stack<T>
  5. Constraints: <T extends SomeType>
  6. keyof: keys of object types

Next steps



자주 묻는 질문 (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, Generics, Type Parameters, Constraints, keyof 등으로 검색하시면 이 글이 도움이 됩니다.