← 가이드 목록으로 ← Back to guides

TypeScript 제네릭과 타입 프로그래밍 완벽 가이드 TypeScript Generics & Type Programming Complete Guide

제네릭은 TypeScript의 핵심 기능으로, 재사용 가능하고 타입 안전한 코드를 작성할 수 있게 해줍니다. 제네릭 함수, 클래스, 제약 조건부터 실전 유틸리티 타입까지 체계적으로 배워봅시다.

Generics are a core TypeScript feature that enables reusable and type-safe code. Let's systematically learn from generic functions, classes, constraints to practical utility types.

제네릭 함수 기초 Generic Function Basics

제네릭을 사용하면 호출 시점에 타입이 결정되어, 다양한 타입에서 작동하는 함수를 작성할 수 있습니다.

Generics allow types to be determined at call time, enabling functions that work with various types.

// 기본 제네릭 함수
function identity<T>(value: T): T {
  return value;
}

// 타입 추론
const str = identity('hello'); // string
const num = identity(42); // number

// 명시적 타입 지정
const explicit = identity<boolean>(true);
// Basic generic function
function identity<T>(value: T): T {
  return value;
}

// Type inference
const str = identity('hello'); // string
const num = identity(42); // number

// Explicit type specification
const explicit = identity<boolean>(true);

다중 타입 파라미터 Multiple Type Parameters

// 두 가지 타입 파라미터
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

const result = pair('name', 25);
// [string, number]

// 객체 키-값 유틸리티
function getProperty<T, K extends keyof T>(
  obj: T,
  key: K
): T[K] {
  return obj[key];
}

const user = { name: 'Kim', age: 30 };
const name = getProperty(user, 'name'); // string
// Two type parameters
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

const result = pair('name', 25);
// [string, number]

// Object key-value utility
function getProperty<T, K extends keyof T>(
  obj: T,
  key: K
): T[K] {
  return obj[key];
}

const user = { name: 'Kim', age: 30 };
const name = getProperty(user, 'name'); // string

제네릭 제약 조건 (Constraints) Generic Constraints

extends 키워드로 제네릭 타입에 제약을 걸어 특정 구조를 보장할 수 있습니다.

Use the extends keyword to constrain generic types and guarantee specific structures.

// length 속성을 가진 타입만 허용
interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(item: T): T {
  console.log(`길이: ${item.length}`);
  return item;
}

logLength('hello'); // ✅ 문자열
logLength([1, 2, 3]); // ✅ 배열
// logLength(123); // ❌ 에러: number에는 length 없음
// Only allow types with length property
interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(item: T): T {
  console.log(`Length: ${item.length}`);
  return item;
}

logLength('hello'); // ✅ String
logLength([1, 2, 3]); // ✅ Array
// logLength(123); // ❌ Error: number has no length

제네릭 클래스와 인터페이스 Generic Classes & Interfaces

// 제네릭 인터페이스
interface Repository<T> {
  getById(id: string): T | null;
  getAll(): T[];
  save(item: T): void;
}

// 제네릭 클래스
class DataStore<T> implements Repository<T> {
  private items: Map<string, T> = new Map();

  getById(id: string): T | null {
    return this.items.get(id) ?? null;
  }

  getAll(): T[] {
    return Array.from(this.items.values());
  }

  save(item: T & { id: string }): void {
    this.items.set(item.id, item);
  }
}

// 사용 예시
interface User { id: string; name: string; }
const userStore = new DataStore<User>();
// Generic interface
interface Repository<T> {
  getById(id: string): T | null;
  getAll(): T[];
  save(item: T): void;
}

// Generic class
class DataStore<T> implements Repository<T> {
  private items: Map<string, T> = new Map();

  getById(id: string): T | null {
    return this.items.get(id) ?? null;
  }

  getAll(): T[] {
    return Array.from(this.items.values());
  }

  save(item: T & { id: string }): void {
    this.items.set(item.id, item);
  }
}

// Usage example
interface User { id: string; name: string; }
const userStore = new DataStore<User>();

기본 타입 (Default Types) Default Types

// 기본값이 있는 제네릭
interface ApiResponse<T = unknown, E = Error> {
  data: T | null;
  error: E | null;
  status: number;
}

// 모든 타입 파라미터 생략 가능
const response1: ApiResponse = {
  data: null,
  error: new Error('실패'),
  status: 500
};

// 일부만 지정
const response2: ApiResponse<User> = {
  data: { id: '1', name: 'Kim' },
  error: null,
  status: 200
};
// Generics with defaults
interface ApiResponse<T = unknown, E = Error> {
  data: T | null;
  error: E | null;
  status: number;
}

// Can omit all type parameters
const response1: ApiResponse = {
  data: null,
  error: new Error('Failed'),
  status: 500
};

// Specify only some
const response2: ApiResponse<User> = {
  data: { id: '1', name: 'Kim' },
  error: null,
  status: 200
};

실전 유틸리티 타입 만들기 Building Practical Utility Types

// Nullable<T>: null 허용
type Nullable<T> = T | null;

// AsyncReturnType: 비동기 함수 반환 타입 추출
type AsyncReturnType<T extends (...args: any) => Promise<any>> =
  T extends (...args: any) => Promise<infer R> ? R : never;

// 사용 예시
async function fetchUser() {
  return { id: '1', name: 'Kim' };
}
type UserType = AsyncReturnType<typeof fetchUser>;
// { id: string; name: string; }
// Nullable<T>: Allow null
type Nullable<T> = T | null;

// AsyncReturnType: Extract async function return type
type AsyncReturnType<T extends (...args: any) => Promise<any>> =
  T extends (...args: any) => Promise<infer R> ? R : never;

// Usage example
async function fetchUser() {
  return { id: '1', name: 'Kim' };
}
type UserType = AsyncReturnType<typeof fetchUser>;
// { id: string; name: string; }

고급 유틸리티 패턴 Advanced Utility Patterns

// DeepReadonly: 중첩 객체까지 읽기 전용
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? DeepReadonly<T[K]>
    : T[K];
};

// StrictOmit: 존재하지 않는 키 사용 시 에러
type StrictOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

// Mutable: readonly 제거
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};
// DeepReadonly: Readonly for nested objects
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? DeepReadonly<T[K]>
    : T[K];
};

// StrictOmit: Error on non-existent keys
type StrictOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

// Mutable: Remove readonly
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

✨ 제네릭 Best Practices ✨ Generics Best Practices

  • T, U, K 등 관례적인 이름 사용 (또는 의미있는 이름)
  • 타입 추론이 가능하면 명시적 타입 지정 생략
  • extends로 제약을 걸어 타입 안전성 확보
  • 너무 복잡한 제네릭은 타입 별칭으로 분리
  • Use conventional names like T, U, K (or meaningful names)
  • Omit explicit types when inference works
  • Use extends for constraints to ensure type safety
  • Extract complex generics into type aliases

⚠️ 흔한 실수들 ⚠️ Common Mistakes

  • 제약 없이 T의 속성에 접근 시도
  • 제네릭 대신 anyunknown 남용
  • 과도하게 복잡한 제네릭으로 가독성 저하
  • Accessing properties of T without constraints
  • Overusing any or unknown instead of generics
  • Overly complex generics hurting readability