icon
3장 : 함수와 타입

함수 오버로딩


자바스크립트에서는 동일한 이름의 함수를 여러 개 정의할 수 없으며, 마지막에 정의된 함수가 이전 함수를 덮어씁니다. 하지만 때로는 하나의 함수가 서로 다른 타입의 인자를 받았을 때 다르게 동작하거나, 다른 타입의 결과를 반환해야 하는 경우가 있습니다. 예를 들어, add 함수가 숫자를 더할 수도 있고, 문자열을 연결할 수도 있는 것처럼 말이죠.

타입스크립트는 이러한 시나리오를 타입 안전하게 다룰 수 있도록 함수 오버로딩(Function Overloading) 기능을 제공합니다. 함수 오버로딩은 하나의 함수에 대해 여러 개의 함수 시그니처(Function Signatures) 를 정의하고, 마지막에 실제 구현체를 제공하는 방식입니다.


함수 오버로딩의 기본 개념

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

오버로드 시그니처 (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"));  // 17
console.log(getLength(["apple", "banana"])); // 2

// 오류: 정의된 오버로드 시그니처에 맞지 않는 인자 타입
// console.log(getLength(123)); // Error: 'number' 형식의 인수는 'string | string[]' 형식의 매개 변수에 할당될 수 없습니다.

동작 방식

  • 컴파일 시점: 타입스크립트 컴파일러는 함수를 호출할 때 제공된 인자들의 타입을 기반으로, 정의된 여러 오버로드 시그니처 중에서 가장 적합한 것을 찾아 타입 검사를 수행합니다. getLength("Hello")를 호출하면 function getLength(input: string): number; 시그니처에 따라 검사됩니다.
  • 런타임: 실제 실행되는 코드는 구현 시그니처에 해당하는 하나의 함수뿐입니다. 구현 시그니처 내부에서는 typeofinstanceof 같은 타입 가드(Type Guard) 를 사용하여 인자의 실제 런타임 타입을 확인하고, 그에 맞는 로직을 실행합니다.

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

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

구현 시그니처는 항상 오버로드 시그니처를 포괄해야 합니다: 구현 시그니처의 매개변수 타입은 모든 오버로드 시그니처의 매개변수 타입을 수용할 수 있는 유니온 타입이어야 하며, 반환 타입 또한 마찬가지입니다. 만약 구현 시그니처가 특정 오버로드 시그니처를 처리할 수 없다면 컴파일 오류가 발생합니다.

// 올바른 예시:
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 사용)

순서의 중요성: 오버로드 시그니처의 순서도 중요할 수 있습니다. 특히 더 구체적인 시그니처가 더 일반적인 시그니처보다 먼저 와야 합니다. 그렇지 않으면 일반적인 시그니처가 먼저 매칭되어 의도치 않은 결과를 초래할 수 있습니다.

// 더 구체적인 시그니처가 먼저 와야 합니다.
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; // 이 오버로드는 절대 매칭되지 않음

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

함수 오버로딩과 유니온 타입(예: 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
    let res2 = processUnion(10);    // number
  • 함수 오버로딩: 동일한 함수 이름에 대해 여러 개의 독립적인 호출 시그니처를 제공합니다. 각 오버로드 시그니처는 특정 인자 타입 조합에 대한 특정 반환 타입을 명확히 정의합니다. 컴파일러는 호출 시 가장 적합한 시그니처를 찾아내어 해당 시그니처의 반환 타입을 추론합니다. 이는 함수 호출부에서 더 정확한 반환 타입 정보를 얻을 수 있다는 장점이 있습니다.

    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 (정확히 추론)

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

언제 오버로딩을 사용해야 할까요?

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

하지만 모든 경우에 오버로딩이 필요한 것은 아닙니다. 간단한 경우나, 반환 타입이 인자 타입에 크게 종속되지 않는다면 유니온 타입을 사용하는 것이 더 간결할 수 있습니다.


함수 오버로딩은 하나의 함수가 여러 형태의 동작을 수행할 때 타입 안정성을 확보하면서 코드의 가독성을 높이는 고급 기능입니다. 특히 라이브러리나 복잡한 유틸리티 함수를 작성할 때 그 진가를 발휘합니다. 오버로드 시그니처와 구현 시그니처의 역할을 명확히 이해하고, 타입 가드를 적절히 활용하는 것이 중요합니다.