제네릭(Generic)은 타입을 매개변수처럼 사용하는 것입니다. 함수를 호출할 때 인수를 넘기듯, 타입도 "넘겨서" 재사용 가능한 코드를 만듭니다.

왜 제네릭이 필요한가

TYPESCRIPT
// ❌ any — 타입 정보가 사라짐
function identity(value: any): any {
  return value;
}
const result = identity('hello'); // result: any — string 정보를 잃어버림

// ❌ 타입별로 함수를 만드는 건 비효율적
function identityString(value: string): string { return value; }
function identityNumber(value: number): number { return value; }

// ✅ 제네릭 — 타입을 매개변수로
function identityGeneric<T>(value: T): T {
  return value;
}

const str = identityGeneric('hello');  // str: 'hello'
const num = identityGeneric(42);       // num: 42

제네릭 함수

TYPESCRIPT
// T는 관례적으로 사용하는 타입 매개변수 이름
function wrap<T>(value: T): { value: T } {
  return { value };
}

// 호출 시 타입을 명시하거나 추론에 맡기기
const a = wrap<string>('hello');  // 명시: { value: string }
const b = wrap(42);               // 추론: { value: number }

// 여러 타입 매개변수
function pair<A, B>(first: A, second: B): [A, B] {
  return [first, second];
}

const p = pair('hello', 42); // [string, number]

화살표 함수에서 제네릭

TYPESCRIPT
// 일반적인 화살표 함수 제네릭
const identity = <T>(value: T): T => value;

// TSX 파일에서는 <T>가 JSX 태그로 인식될 수 있으므로
// extends를 붙이는 트릭을 사용
const identityTsx = <T extends unknown>(value: T): T => value;
// 또는 trailing comma
const identityTsx2 = <T,>(value: T): T => value;

제네릭 인터페이스

TYPESCRIPT
// API 응답 타입을 제네릭으로 정의
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

// 다양한 응답 타입에 재사용
interface User {
  id: number;
  name: string;
}

interface Product {
  id: number;
  title: string;
  price: number;
}

type UserResponse = ApiResponse<User>;
// { data: User; status: number; message: string }

type ProductResponse = ApiResponse<Product>;
// { data: Product; status: number; message: string }

type ListResponse = ApiResponse<User[]>;
// { data: User[]; status: number; message: string }

기본 타입 매개변수

TYPESCRIPT
// 기본값을 지정할 수 있음
interface Container<T = string> {
  value: T;
}

const c1: Container = { value: 'hello' };       // T = string (기본값)
const c2: Container<number> = { value: 42 };     // T = number (명시)

제네릭 클래스

TYPESCRIPT
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];
  }

  get size(): number {
    return this.items.length;
  }
}

// 문자열 스택
const stringStack = new Stack<string>();
stringStack.push('hello');
stringStack.push('world');
const top = stringStack.pop(); // string | undefined

// 숫자 스택
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);

제네릭 타입 별칭

TYPESCRIPT
// 트리 구조를 제네릭으로
type TreeNode<T> = {
  value: T;
  children: TreeNode<T>[];
};

const tree: TreeNode<string> = {
  value: '루트',
  children: [
    { value: '자식1', children: [] },
    { value: '자식2', children: [
      { value: '손자1', children: [] },
    ]},
  ],
};

// Nullable 유틸리티
type Nullable<T> = T | null;
type Optional<T> = T | undefined;

const username: Nullable<string> = null; // OK

제네릭과 배열 메서드

사실 이미 제네릭을 매일 쓰고 있습니다. Array.map, Promise, useState 모두 제네릭입니다.

TYPESCRIPT
// Array<T>.map 내부 시그니처
// map<U>(callbackfn: (value: T, index: number) => U): U[]

const numbers = [1, 2, 3]; // number[]
const strings = numbers.map((n) => String(n)); // string[]
// map<string>으로 추론됨

// Promise<T>
async function fetchUser(): Promise<User> {
  const response = await fetch('/api/user');
  return response.json();
}

제네릭 네이밍 관례

TYPESCRIPT
// T — 일반적인 타입
function identity<T>(value: T): T { return value; }

// K, V — 키/값
function getProperty<K extends keyof T, T>(obj: T, key: K): T[K] {
  return obj[key];
}

// E — 요소(Element)
interface Collection<E> {
  add(element: E): void;
  get(index: number): E;
}

// R — 반환 타입(Return)
type Fn<T, R> = (arg: T) => R;

하나의 문자 대신 의미 있는 이름을 쓸 수도 있습니다.

TYPESCRIPT
// 긴 이름 — 복잡한 제네릭에서 가독성이 좋음
interface Repository<Entity, Id = number> {
  findById(id: Id): Entity | null;
  save(entity: Entity): Id;
}

정리

  • 제네릭은 "타입의 매개변수"로, 재사용 가능한 타입 안전 코드를 만든다
  • 호출 시 타입을 명시하거나 추론에 맡길 수 있다
  • 기본 타입 매개변수로 생략 시 기본값을 지정할 수 있다
  • Array, Promise, React의 useState 등 이미 제네릭을 매일 사용하고 있다
  • 다음 편에서 extends로 제네릭을 제약하는 방법을 다룬다
댓글 로딩 중...