오버로드 vs 제네릭 — 함수 시그니처 설계 전략
함수의 매개변수와 반환 타입을 유연하게 정의하는 두 가지 전략: 오버로드 는 입출력 조합을 명시하고, 제네릭 은 관계를 추상화합니다.
같은 문제, 두 가지 접근
// 문제: 문자열이면 문자열을, 숫자면 숫자를 반환하는 함수
// 방법 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;
}
오버로드가 적합한 경우
입력-출력 관계가 명확히 다를 때
// 매개변수 개수가 다른 경우
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
반환 타입이 완전히 다른 경우
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
제네릭이 적합한 경우
입출력 타입이 동일한 패턴
// 배열의 첫 번째 요소 반환
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
타입 관계를 보존해야 할 때
// 객체의 속성값 가져오기
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
제네릭 제약으로 충분한 경우
// 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
안티패턴: 불필요한 오버로드
// ❌ 오버로드가 불필요한 경우
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 공식 문서에서도 "가능하면 오버로드 대신 유니온이나 제네릭을 쓰라"고 권장하더라고요.
선택 기준 정리
| 상황 | 추천 |
|---|---|
| 매개변수 타입에 따라 반환 타입이 달라짐 | 오버로드 |
| 매개변수 개수가 다른 여러 호출 형태 | 오버로드 |
| 입출력 타입이 동일하거나 관련됨 | 제네릭 |
| 여러 타입을 동일하게 처리 | 유니온 또는 제네릭 |
| 타입 관계를 보존해야 함 | 제네릭 |
// 실무에서는 둘을 조합하기도 함
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 공식 문서는 가능하면 유니온이나 제네릭을 권장한다
- 불필요한 오버로드는 코드만 복잡하게 만든다
- 실무에서는 두 접근을 조합해서 사용하기도 한다
댓글 로딩 중...