iBetter Books
수정

함수 오버로드

햄버거 가게를 생각해봅시다. "세트 번호를 주세요"라고 하면 음식이 나옵니다. "세트 이름을 알려주세요"라고 해도 됩니다. 번호로 주문하면 번호에 맞는 세트, 이름으로 주문하면 이름에 맞는 세트가 나옵니다. 같은 행동인데 입력 방식에 따라 처리가 달라집니다.

TypeScript의 함수 오버로드가 바로 이런 상황을 표현합니다. 같은 함수 이름으로 서로 다른 입력 조합을 처리하면서, 각 경우에 맞는 반환 타입을 정확히 정의할 수 있습니다.

오버로드가 필요한 상황

유니온 타입만으로는 부족한 경우가 있습니다. 다음 예를 봅시다.

// 새 파일: overload-problem.ts// 문제: 유니온 타입으로는 입력-출력 관계를 표현하기 어렵습니다function format(value: string | number): string | number {  if (typeof value === "string") {    return value.toUpperCase();   // string을 넣으면 string이 나와야 합니다  }  return value.toFixed(2);       // number를 넣으면 string이 나옵니다}const result = format("hello");   // 타입이 string | number — 불편합니다// result.toUpperCase()를 호출하려면 타입 단언이 필요합니다

format("hello")의 결과가 string | number로 추론됩니다. 실제로는 항상 string인데도 TypeScript는 그 사실을 모릅니다. 오버로드를 쓰면 이 관계를 정확히 표현할 수 있습니다.

오버로드 시그니처와 구현 시그니처

오버로드는 두 부분으로 구성됩니다. 먼저 오버로드 시그니처를 여러 개 선언하고, 마지막에 실제 구현을 담은 구현 시그니처를 씁니다.

// 새 파일: overload-basic.ts// 오버로드 시그니처 (선언만, 구현 없음)function format(value: string): string;function format(value: number): string;// 구현 시그니처 (실제 코드)function format(value: string | number): string {  if (typeof value === "string") {    return value.toUpperCase();  }  return value.toFixed(2);}const result1 = format("hello");   // string으로 추론됩니다const result2 = format(3.14159);   // string으로 추론됩니다console.log(result1);   // HELLOconsole.log(result2);   // 3.14console.log(result1.toLowerCase());   // hello (타입 단언 없이 바로 사용 가능)

오버로드 시그니처 두 개 덕분에 format("hello")의 반환 타입이 string으로 정확히 추론됩니다.

중요한 점이 있습니다. 구현 시그니처는 외부에서 호출할 수 없습니다. 오버로드 시그니처에 없는 형태로는 호출이 안 됩니다.

// 구현 시그니처로 직접 호출 불가format(true);   // 오류: string이나 number만 가능합니다

입력에 따라 반환 타입이 달라지는 경우

오버로드의 가장 전형적인 사용 사례입니다.

// 새 파일: overload-return.tsinterface TextResult {  type: "text";  value: string;}interface NumberResult {  type: "number";  value: number;}function parse(input: string): TextResult;function parse(input: number): NumberResult;function parse(input: string | number): TextResult | NumberResult {  if (typeof input === "string") {    return { type: "text", value: input.trim() };  }  return { type: "number", value: Math.floor(input) };}const textResult = parse("  TypeScript  ");console.log(textResult.type);    // textconsole.log(textResult.value);   // TypeScriptconst numberResult = parse(3.9);console.log(numberResult.type);    // numberconsole.log(numberResult.value);   // 3// TypeScript는 각 경우의 반환 타입을 정확히 알고 있습니다// textResult.type은 "text"이고 numberResult.type은 "number"입니다

매개변수 개수가 다른 경우

같은 함수가 인수 개수에 따라 다르게 동작하는 경우도 오버로드로 표현합니다.

// 새 파일: overload-params.ts// 날짜 만들기: 하나씩 또는 객체로function createDate(timestamp: number): Date;function createDate(year: number, month: number, day: number): Date;function createDate(yearOrTimestamp: number, month?: number, day?: number): Date {  if (month !== undefined && day !== undefined) {    return new Date(yearOrTimestamp, month - 1, day);  }  return new Date(yearOrTimestamp);}const date1 = createDate(1706745600000);      // 타임스탬프로 생성const date2 = createDate(2024, 2, 1);         // 년, 월, 일로 생성console.log(date1.toLocaleDateString("ko-KR"));   // 2024. 2. 1.console.log(date2.toLocaleDateString("ko-KR"));   // 2024. 2. 1.// 오버로드에 없는 방식으로 호출하면 오류const date3 = createDate(2024, 2);   // 오류: 이 오버로드와 일치하지 않습니다

실전 예제 — getElementById 스타일 함수

DOM의 querySelector처럼 선택자에 따라 반환 타입이 달라지는 함수를 만들어봅시다.

// 새 파일: overload-practical.tsinterface InputElement {  type: "input";  value: string;  focus(): void;}interface ButtonElement {  type: "button";  label: string;  click(): void;}interface SelectElement {  type: "select";  options: string[];  selectedIndex: number;}// 각 ID에 따라 반환 타입이 다릅니다function getElement(id: "username-input"): InputElement;function getElement(id: "submit-btn"): ButtonElement;function getElement(id: "country-select"): SelectElement;function getElement(id: string): InputElement | ButtonElement | SelectElement {  // 실제 구현 (예시)  if (id === "username-input") {    return { type: "input", value: "", focus() {} };  }  if (id === "submit-btn") {    return { type: "button", label: "제출", click() {} };  }  return { type: "select", options: [], selectedIndex: 0 };}const input = getElement("username-input");input.focus();    // InputElement의 메서드 — 타입 단언 없이 바로 사용 가능const button = getElement("submit-btn");button.click();   // ButtonElement의 메서드const select = getElement("country-select");console.log(select.options);   // SelectElement의 속성

오버로드와 유니온 타입, 언제 무엇을 쓸까

오버로드가 항상 정답은 아닙니다. 단순한 경우에는 유니온 타입이 더 명확합니다.

// 유니온 타입으로 충분한 경우function printValue(value: string | number): void {  console.log(String(value));}// 반환 타입이 입력에 따라 달라지지 않으므로 오버로드 불필요// 오버로드가 필요한 경우function convert(value: string): number;function convert(value: number): string;function convert(value: string | number): string | number {  if (typeof value === "string") return Number(value);  return String(value);}// 입력 타입에 따라 반환 타입이 달라집니다 — 오버로드 적합

오버로드를 쓸 기준을 정리하면 이렇습니다. 입력 타입에 따라 반환 타입이 달라진다면 오버로드를 씁니다. 반환 타입이 항상 같다면 유니온 타입으로 충분합니다.


오버로드는 TypeScript에서 비교적 고급 기법입니다. 처음에는 복잡해 보이지만, 라이브러리 코드나 유틸리티 함수를 작성할 때 호출하는 쪽의 타입 경험을 크게 개선합니다.

다음 장에서는 화살표 함수와 메서드에 타입을 붙이는 방법을 정리합니다. 특히 TypeScript에서 가장 헷갈리는 주제 중 하나인 this 타이핑도 다룹니다.