본문 바로가기

함수 오버로딩

하나의 구현에 여러 호출 시그니처를 선언해 인자별 반환 타입을 보존하고 유니온 매개변수와의 선택 기준을 비교합니다.

자바스크립트에는 인자 타입별로 공개 시그니처를 나누는 함수 오버로드 문법이 없습니다. 일반 script의 같은 범위에서 반복 함수 선언이 허용되는 경우에도 여러 구현이 선택되는 것이 아니라 마지막 선언의 구현이 사용됩니다. 모듈이나 블록 등에서는 중복 선언이 문법 오류가 될 수 있으므로, 모든 환경에서 같은 이름을 다시 선언할 수 있다고 보지는 않습니다.

하지만 때로는 하나의 함수가 서로 다른 타입의 인자를 받았을 때 다르게 동작하거나, 다른 타입의 결과를 반환해야 하는 경우가 있습니다.

예를 들어, add 함수가 숫자를 더할 수도 있고, 문자열을 연결할 수도 있는 것처럼 말이죠.

타입스크립트는 이러한 시나리오를 타입 안전하게 다룰 수 있도록 함수 오버로딩(Function Overloading) 기능을 제공합니다.

함수 오버로딩은 하나의 함수에 대해 여러 개의 함수 시그니처(Function Signatures)를 정의하고, 마지막에 실제 구현체를 제공하는 방식입니다.


함수 오버로딩의 기본 개념

공개 string과 string[] 시그니처는 모두 number를 반환합니다. 실행 구현은 하나이며 getLength 결과는 문자열 길이 16과 배열 길이 2입니다.

TYPE / RUNTIME / CONTRACT

공개 오버로드로 검사하고 하나의 구현을 실행합니다

getLength는 string과 string[] 호출을 각각 공개합니다. 구현의 더 넓은 매개변수 타입은 호출자가 선택할 추가 후보가 아닙니다.

두 공개 계약과 하나의 구현공개string→구현및공개string[]→같은구현의3노드2간선이다. 각호출은공개후보로검사되고하나의실제함수에서실행된다. 공개후보끼리순차실행하거나두함수본문을생성하지않는다.공개 string반환: number공개 string[]반환: number하나의 구현실제 input.length
두 공개 계약과 하나의 구현 · 모바일공개string→구현및공개string[]→같은구현의3노드2간선이다. 각호출은공개후보로검사되고하나의실제함수에서실행된다. 공개후보끼리순차실행하거나두함수본문을생성하지않는다.공개 string반환: number공개 string[]반환: number하나의 구현실제 input.length호출 후보는 별도

PUBLIC CALL

유니온 인수가 자동 분배되지는 않습니다

string | string[] 변수는 어느 공개 후보에도 그대로 맞지 않아 오류입니다. 호출 전에 좁히거나 유니온 공개 시그니처를 설계해야 합니다.

IMPLEMENTATION

모든 공개 계약과 호환

구현은 유니온·unknown·제네릭 등으로 각 후보를 처리할 수 있어야 합니다. 타입 선언이 런타임 분기를 자동 생성하지는 않습니다.

RELATION

입력에 따른 반환 타입

processOverload("abc")는 string, 숫자 호출은 number입니다. 본문의 유니온 반환 함수는 두 호출 모두 string | number입니다.

CHOICE

필요한 관계를 간결하게 표현

반환이 같은 getLength는 유니온 매개변수도 가능합니다. 구체적인 후보 우선 배치는 유용하지만 단일 문자열 리터럴 후보 같은 선택 규칙도 있어 단순히 첫 줄만 고른다고 가정하지 않습니다.

오버로드 선언은 검사 시점의 계약입니다. JavaScript에는 실제 구현 함수 하나만 남습니다. 넓은 string 입력의 실제 결과가 문자열일 수도 있는 processInputSafe는 본문에서 string | number 계약으로 따로 보완합니다.

함수 오버로딩은 다음과 같은 구조를 가집니다.

오버로드 시그니처 (Overload Signatures): 함수의 다양한 호출 방식을 선언합니다.

이들은 실제 구현부가 없으며, 매개변수 타입과 반환 타입만을 명시합니다.

여러 개를 정의할 수 있습니다.

구현 시그니처 (Implementation Signature): 모든 오버로드 시그니처를 포괄할 수 있는 가장 일반적인 타입으로 함수의 실제 구현부를 정의합니다.

구현 시그니처는 공개된 각 오버로드의 매개변수와 반환 계약에 호환되어야 합니다.

즉, 오버로드 시그니처의 모든 경우를 처리할 수 있어야 합니다.

예시를 통해 살펴보겠습니다.

문자열이나 문자열 배열을 입력받아 길이를 반환하는 getLength 함수를 만들어보겠습니다.

// 1. 오버로드 시그니처 (Overload Signatures)
// 첫 번째 오버로드: 인자가 string 타입일 때 number를 반환
function getLength(input: string): number;
// 두 번째 오버로드: 인자가 string[] (문자열 배열) 타입일 때 number를 반환
function getLength(input: string[]): number;

// 2. 구현 시그니처 (Implementation Signature)
// 실제 함수 로직을 포함하며, 모든 오버로드 시그니처를 포괄할 수 있는 타입을 가집니다.
function getLength(input: string | string[]): number {
  if (typeof input === "string") {
    return input.length; // string일 경우 length 속성 사용
  } else {
    return input.length; // string[]일 경우 length 속성 사용
  }
}

// 함수 호출 (오버로드 시그니처에 따라 타입 검사가 이루어집니다.)
console.log(getLength("Hello TypeScript"));  // 16
console.log(getLength(["apple", "banana"])); // 2

// 오류: 정의된 오버로드 시그니처에 맞지 않는 인자 타입
// console.log(getLength(123)); // Error: 'number' 형식의 인수는 'string | string[]' 형식의 매개 변수에 할당될 수 없습니다.
동작 방식
  • 컴파일 시점: 타입스크립트 컴파일러는 함수를 호출할 때 제공된 인자들의 타입을 기반으로, 정의된 여러 오버로드 시그니처 중에서 가장 적합한 것을 찾아 타입 검사를 수행합니다. getLength("Hello")를 호출하면 function getLength(input: string): number; 시그니처에 따라 검사됩니다.
  • 런타임: 실제 실행되는 코드는 구현 시그니처에 해당하는 하나의 함수뿐입니다. 구현 시그니처 내부에서는 typeof나 instanceof 같은 타입 가드(Type Guard)를 사용하여 인자의 실제 런타임 타입을 확인하고, 그에 맞는 로직을 실행합니다.

함수 오버로딩의 규칙 및 주의사항

함수 오버로딩을 올바르게 사용하기 위해서는 몇 가지 중요한 규칙을 이해해야 합니다.

구현 시그니처는 항상 오버로드 시그니처를 포괄해야 합니다: 유니온은 여러 공개 계약을 수용하는 한 방법입니다. unknown이나 제네릭 등을 사용할 수도 있으므로 매개변수와 반환 타입이 반드시 유니온이어야 하는 것은 아닙니다.

만약 구현 시그니처가 특정 오버로드 시그니처를 처리할 수 없다면 컴파일 오류가 발생합니다.

// 올바른 예시:
function overloadExample(x: number): number;
function overloadExample(x: string): string;
function overloadExample(x: number | string): number | string { // number | string 은 위 두 시그니처를 모두 포괄합니다.
  if (typeof x === 'number') return x * 2;
  return x + x;
}

// 잘못된 예시 (구현 시그니처가 오버로드를 포괄하지 못함):
// function invalidOverload(x: number): number;
// function invalidOverload(x: string): string;
// function invalidOverload(x: number): number { // Error: 'string' 오버로드를 처리할 수 없음
//   return x * 2;
// }

오버로드 시그니처만 함수를 호출하는 데 사용됩니다: 타입스크립트는 함수를 호출할 때 오직 오버로드 시그니처만을 참조하여 타입 검사를 수행합니다.

구현 시그니처는 내부적인 구현을 위한 것이며, 직접적으로 호출될 수 있는 타입 정보를 제공하지 않습니다.

// getLength 함수는 `string | string[]` 인자를 받을 수 있다고 정의되었지만,
// 실제 호출할 때는 `string` 또는 `string[]`으로만 호출해야 합니다.
// getLength("hello" as string | string[]); // Error: no overload expects 1 argument...
                                          // 실제로는 이 타입의 오버로드 시그니처가 없기 때문입니다.

이 점 때문에 구현 시그니처는 오버로드 시그니처보다 인자의 수가 더 많거나, 매개변수 타입을 더 넓게 설정해야 할 때가 있습니다. (예: 선택적 매개변수나 any 또는 unknown 사용)

순서의 중요성: 오버로드 시그니처의 순서도 중요할 수 있습니다.

여러 후보가 겹치면 구체적인 계약을 먼저 배치하는 것이 유용합니다. 다만 단일 문자열 리터럴을 포함한 후보처럼 별도의 선택 규칙이 있어 모든 호출이 단순히 첫 번째 일치 행을 고르는 것은 아닙니다.

순서만 바꿔 결과를 추측하기보다 실제 호출 인수의 정적 타입과 추론된 반환 타입을 확인합니다. 아래 원 예제의 직접 리터럴 호출과, 넓은 string 변수를 전달하는 호출은 구분해야 합니다.

// 더 구체적인 시그니처가 먼저 와야 합니다.
function processInput(input: 'hello'): string; // 'hello' 리터럴 타입이 더 구체적
function processInput(input: string): number; // string 일반 타입

function processInput(input: string): string | number {
  if (input === 'hello') {
    return "Special Hello!";
  }
  return input.length;
}

console.log(processInput('hello')); // Special Hello!
console.log(processInput('world')); // 5

// 만약 순서가 바뀌면:
// function processInput(input: string): number;
// function processInput(input: 'hello'): string; // 단일 문자열 리터럴 후보는 우선 선택될 수 있으므로 순서만으로 배제하지 않음

위 processInput의 직접 호출 두 결과는 맞지만, 일반 string 인수에는 number를 반환한다는 원 계약은 충분하지 않습니다. const word: string = ["hello"].join("");로 실제 값이 hello인 넓은 string을 만들면, 원 선언은 processInput(word)의 정적 타입을 number로 선택해도 구현은 문자열 Special Hello!를 반환합니다. 오버로드가 런타임 분기를 생성하거나 입력별 구현의 의미를 자동 증명하지 않기 때문입니다.

이 계약을 사용하는 확장 코드는 넓은 string 결과에 가능한 문자열을 포함해야 합니다. 아래는 원 예제와 이름을 구분한 안전한 공개 계약입니다.

function processInputSafe(input: 'hello'): string;
function processInputSafe(input: string): string | number;
function processInputSafe(input: string): string | number {
  return input === 'hello' ? 'Special Hello!' : input.length;
}
const word: string = ['hello'].join('');
const result = processInputSafe(word); // string | number
if (typeof result === 'number') {
  console.log(result.toFixed(0));
} else {
  console.log(result.toUpperCase()); // SPECIAL HELLO!
}

일반 string은 실행 전 hello 여부가 정적 타입에 고정되지 않으므로 결과도 좁힌 뒤 사용합니다. 원 processOverload처럼 number 입력은 항상 number를 계산하고 string 입력은 항상 string을 계산하는 계약과 비교해 읽습니다. 공식 오버로드 설명도 공개 시그니처와 하나의 구현을 구분합니다.

유니온 타입과 오버로딩의 비교

함수 오버로딩과 유니온 타입(예: function func(arg: string | number))은 유사한 문제를 해결하는 것처럼 보일 수 있지만, 미묘한 차이가 있습니다.

  • 유니온 타입: 인자의 타입이 여러 개 중 어떤 하나가 될 수 있음을 표현합니다. 함수 내부에서 typeof 등의 타입 가드를 사용해 인자의 실제 타입을 좁히고 개별적인 로직을 수행합니다. 반환 타입도 유니온 타입으로 표현됩니다.

    function processUnion(input: string | number): string | number {
      if (typeof input === 'string') {
        return input.toUpperCase();
      }
      return input * 2;
    }
    let res1 = processUnion("abc"); // 정적 타입: string | number, 실행 값: "ABC"
    let res2 = processUnion(10);    // 정적 타입: string | number, 실행 값: 20
  • 함수 오버로딩: 동일한 함수 이름에 대해 여러 개의 독립적인 호출 시그니처를 제공합니다. 각 오버로드 시그니처는 특정 인자 타입 조합에 대한 반환 타입을 명확히 정의합니다. 컴파일러는 호출 시 가장 적합한 시그니처를 찾아 해당 시그니처의 반환 타입을 추론합니다. 그래서 호출부에서 더 정확한 반환 타입 정보를 얻을 수 있습니다.

    function processOverload(input: string): string;
    function processOverload(input: number): number;
    function processOverload(input: string | number): string | number {
        if (typeof input === 'string') {
            return input.toUpperCase();
        }
        return input * 2;
    }
    let res3 = processOverload("abc"); // string (정확히 추론)
    let res4 = processOverload(10);    // number (정확히 추론)

    res3과 res4의 타입 추론 결과를 비교해보면, 오버로딩이 더 정교한 타입 추론을 가능하게 함을 알 수 있습니다.

언제 오버로딩을 사용해야 할까요?
  • 함수의 매개변수 타입 조합에 따라 반환 타입이 명확히 달라지는 경우: 예를 들어, parse(str: string): number와 parse(num: number): string처럼 인자와 반환 타입의 관계가 복잡할 때 유용합니다.
  • 함수 호출부에서 더욱 정확한 반환 타입을 얻고 싶을 때: 유니온 타입보다 호출하는 쪽에서 예측 가능한 타입을 기대할 수 있습니다.
  • API 문서처럼 함수의 다양한 사용법을 명확하게 표현하고 싶을 때.

하지만 모든 경우에 오버로딩이 필요한 것은 아닙니다.

간단한 경우나, 반환 타입이 인자 타입에 크게 종속되지 않는다면 유니온 타입을 사용하는 것이 더 간결할 수 있습니다.


함수 오버로딩은 하나의 함수가 여러 형태의 동작을 수행할 때 타입 안정성을 확보하면서 코드의 가독성을 높이는 고급 기능입니다.

특히 라이브러리나 복잡한 유틸리티 함수를 작성할 때 그 진가를 발휘합니다.

혼자 공개 계약을 선택하기

① 값이 string 또는 string[]일 수 있는 변수를 원 getLength에 그대로 전달할 수 있을까요? 두 입력 모두 number 길이만 필요할 때 더 간단한 공개 매개변수 타입을 정해 보세요. ② processInputSafe의 word 생성값을 world로 바꿉니다. 정적 result 타입과 실제 분기·출력을 적고, 타입을 좁히지 않은 result.toFixed(0)이 왜 거절되는지 설명합니다.

호출 후보와 반환값 해설

① 원 getLength의 공개 후보는 string 하나와 string[] 하나이므로, 좁혀지지 않은 유니온 값은 어느 한 후보를 만족하지 않습니다. function lengthOf(input: string | string[]): number처럼 공개 유니온 매개변수를 쓰면 이 경우를 직접 받을 수 있습니다. ② word의 정적 타입은 여전히 string이어서 result는 string|number이며, 실제 값은 world의 길이 5이므로 숫자 분기에서 5가 출력됩니다. 호출 결과의 가능성을 확인하기 전에는 문자열일 수 있으므로 숫자 메서드를 바로 사용할 수 없습니다.