함수의 매개변수와 반환 타입을 유연하게 정의하는 두 가지 전략: 오버로드 는 입출력 조합을 명시하고, 제네릭 은 관계를 추상화합니다.

같은 문제, 두 가지 접근

TYPESCRIPT
// 문제: 문자열이면 문자열을, 숫자면 숫자를 반환하는 함수

// 방법 1: 오버로드
function double(value: string): string;
function double(value: number): number;
function double(value: string | number): string | number {
  if (typeof value === 'string') return value + value;
  return value * 2;
}

// 방법 2: 제네릭 + 조건부 타입
function double2<T extends string | number>(
  value: T
): T extends string ? string : number {
  if (typeof value === 'string') return (value + value) as any;
  return (value as number * 2) as any;
}

오버로드가 적합한 경우

입력-출력 관계가 명확히 다를 때

TYPESCRIPT
// 매개변수 개수가 다른 경우
function createElement(tag: 'div'): HTMLDivElement;
function createElement(tag: 'input', type: string): HTMLInputElement;
function createElement(tag: string, type?: string): HTMLElement {
  const el = document.createElement(tag);
  if (type && el instanceof HTMLInputElement) {
    el.type = type;
  }
  return el;
}

const div = createElement('div');            // HTMLDivElement
const input = createElement('input', 'text'); // HTMLInputElement

반환 타입이 완전히 다른 경우

TYPESCRIPT
function parse(input: string, format: 'json'): object;
function parse(input: string, format: 'number'): number;
function parse(input: string, format: 'boolean'): boolean;
function parse(input: string, format: string): unknown {
  switch (format) {
    case 'json': return JSON.parse(input);
    case 'number': return Number(input);
    case 'boolean': return input === 'true';
    default: return input;
  }
}

const obj = parse('{"a":1}', 'json');   // object
const num = parse('42', 'number');       // number
const bool = parse('true', 'boolean');   // boolean

제네릭이 적합한 경우

입출력 타입이 동일한 패턴

TYPESCRIPT
// 배열의 첫 번째 요소 반환
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

const a = first([1, 2, 3]);       // number | undefined
const b = first(['a', 'b']);       // string | undefined

타입 관계를 보존해야 할 때

TYPESCRIPT
// 객체의 속성값 가져오기
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: '홍길동', age: 25 };
const name = getProperty(user, 'name'); // string
const age = getProperty(user, 'age');   // number

제네릭 제약으로 충분한 경우

TYPESCRIPT
// length가 있는 타입이면 모두 받기
function getLength<T extends { length: number }>(value: T): number {
  return value.length;
}

getLength('hello');    // OK
getLength([1, 2, 3]);  // OK
getLength({ length: 5 }); // OK

안티패턴: 불필요한 오버로드

TYPESCRIPT
// ❌ 오버로드가 불필요한 경우
function len(s: string): number;
function len(arr: any[]): number;
function len(x: string | any[]): number {
  return x.length;
}

// ✅ 유니온으로 충분
function len2(x: string | any[]): number {
  return x.length;
}

공부하다 보니 TypeScript 공식 문서에서도 "가능하면 오버로드 대신 유니온이나 제네릭을 쓰라"고 권장하더라고요.

선택 기준 정리

상황추천
매개변수 타입에 따라 반환 타입이 달라짐오버로드
매개변수 개수가 다른 여러 호출 형태오버로드
입출력 타입이 동일하거나 관련됨제네릭
여러 타입을 동일하게 처리유니온 또는 제네릭
타입 관계를 보존해야 함제네릭
TYPESCRIPT
// 실무에서는 둘을 조합하기도 함
function fetch<T>(url: string): Promise<T>;
function fetch<T>(url: string, options: RequestInit): Promise<T>;
function fetch<T>(url: string, options?: RequestInit): Promise<T> {
  return window.fetch(url, options).then(r => r.json());
}

정리

  • 오버로드는 입력-출력 매핑이 명확하고 다를 때 사용한다
  • 제네릭은 타입 관계를 보존하고 추상화할 때 사용한다
  • TypeScript 공식 문서는 가능하면 유니온이나 제네릭을 권장한다
  • 불필요한 오버로드는 코드만 복잡하게 만든다
  • 실무에서는 두 접근을 조합해서 사용하기도 한다
댓글 로딩 중...