본문 바로가기
C.W.K.
Stream
Lesson 02 of 05 · published

함수 오버로드: 한 이름에 여러 호출 계약

~11 min · functions, overloads, one-name-many-signatures

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"오버로드는 한 구현이 정말 여러 얼굴로 호출될 때 쓰는 도구야. 단지 입력 타입이 둘이라는 이유만으로 꺼내진 마."

공개 시그니처와 구현 시그니처

함수 오버로드는 먼저 호출자가 볼 시그니처를 여러 개 적고, 마지막에 그 모든 경우를 처리하는 구현 하나를 둬. 호출자는 공개된 오버로드만 사용할 수 있고 구현 시그니처는 직접 보지 못해. 구현이 넓게 unknown이나 유니언을 받더라도 허용되는 호출이 자동으로 늘지는 않아.

구현은 모든 오버로드 입력을 받아 처리할 수 있어야 하고, 각 분기에서 약속한 반환값을 만들어야 해. 공개 계약과 실제 분기 사이가 어긋나면 타입 표면은 그럴듯하지만 실행에서 틀린 결과가 나올 수 있으니 테스트도 필요해.

유니언과 제네릭을 먼저 검토해

입력 타입만 여러 개이고 반환 타입이 같다면 value: number | Date 같은 유니언 하나가 더 간단할 때가 많아. 입력 타입과 반환 타입의 관계를 그대로 보존하려면 제네릭이 잘 맞아. wrap<T>(x: T): Box<T>는 가능한 타입마다 오버로드를 늘릴 필요가 없어.

오버로드가 빛나는 자리는 호출 모양 자체가 다를 때야. 인자 개수가 다르거나, 특정 입력 모양에 따라 반환 모양이 뚜렷이 달라지고 한 시그니처로 표현하면 호출자가 쓰기 어려워지는 경우지. 편집기의 자동 완성에 각 호출 형태를 별도 문서처럼 보여 주고 싶을 때도 가치가 있어.

유니언으로 충분하면 유니언, 관계를 보존하면 제네릭, 정말 다른 호출 계약만 오버로드. 이 순서가 API를 짧고 확장 가능하게 지켜 줘.

피파의 고백

오버로드를 쓰면 타입을 정교하게 만든 기분이 들어. 그래서 과하게 쓰기 쉬워. 나중에 새 경우 하나를 추가하며 시그니처 다섯 개를 고치게 되면, 사실 원한 것은 제네릭 하나였다는 걸 자주 깨닫지.

Code

Overload — return 이 어느 형태 호출하느냐에 따라 다를 때·typescript
// 진짜 overload — 다른 모양, 다른 return 타입.

function findUser(id: number): User;
function findUser(query: { email: string }): User[];
function findUser(arg: number | { email: string }): User | User[] {
  if (typeof arg === 'number') {
    return getUserById(arg);             // 단일 User 반환
  } else {
    return searchUsersByEmail(arg.email); // User[] 반환
  }
}

const u: User = findUser(1);             // ✅ 첫 번째 overload 선택
const us: User[] = findUser({ email: 'a@b.c' });  // ✅ 두 번째 선택

// 구현 signature 는 직접 호출 안 됨.
// findUser('hello');                    // ❌ matching overload 없음
대부분 'overload-shaped' 문제가 generic 이나 conditional 타입 원함·typescript
// 반례: union + generic 보통 충분.

// Overload 가치 없음 — 그냥 param 을 union.
function format(v: number | Date): string {
  return v instanceof Date ? v.toISOString() : v.toFixed(2);
}

// Overload 가치 없음 — generic + conditional.
type WrapReturn<T> = T extends string ? { kind: 'str'; value: T } : { kind: 'num'; value: T };
function wrap<T extends string | number>(x: T): WrapReturn<T> {
  return (typeof x === 'string' ? { kind: 'str', value: x } : { kind: 'num', value: x }) as WrapReturn<T>;
}

const a = wrap('hi');   // a: { kind: 'str'; value: 'hi' }
const b = wrap(42);     // b: { kind: 'num'; value: 42 }

External links

Exercise

문자열 하나를 받으면 쉼표로 나눈 배열을 반환하고, 두 번째 인자로 true를 받으면 JSON을 해석하는 parse 오버로드를 작성해. 이어서 제네릭과 조건부 반환 타입을 쓰는 단일 함수로 다시 만들고, 어느 쪽이 더 읽기 쉽고 확장하기 쉬운지 비교해.
Hint
오버로드는 여러 호출 모양을 문서처럼 보여 주고 제네릭은 관계를 조합하기 좋아. 프로젝트의 기본 원칙을 정하되 구체 문제에서 더 단순한 쪽을 선택해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.