함수 오버로딩
자바스크립트에서는 동일한 이름의 함수를 여러 개 정의할 수 없으며, 마지막에 정의된 함수가 이전 함수를 덮어씁니다. 하지만 때로는 하나의 함수가 서로 다른 타입의 인자를 받았을 때 다르게 동작하거나, 다른 타입의 결과를 반환해야 하는 경우가 있습니다. 예를 들어, 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;시그니처에 따라 검사됩니다. - 런타임: 실제 실행되는 코드는 구현 시그니처에 해당하는 하나의 함수뿐입니다. 구현 시그니처 내부에서는
typeof나instanceof같은 타입 가드(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 (정확히 추론)res3과res4의 타입 추론 결과를 비교해보면, 오버로딩이 더 정교한 타입 추론을 가능하게 함을 알 수 있습니다.
- 함수의 매개변수 타입 조합에 따라 반환 타입이 명확히 달라지는 경우: 예를 들어,
parse(str: string): number와parse(num: number): string처럼 인자와 반환 타입의 관계가 복잡할 때 유용합니다. - 함수 호출부에서 더욱 정확한 반환 타입을 얻고 싶을 때: 유니온 타입보다 호출하는 쪽에서 예측 가능한 타입을 기대할 수 있습니다.
- API 문서처럼 함수의 다양한 사용법을 명확하게 표현하고 싶을 때.
하지만 모든 경우에 오버로딩이 필요한 것은 아닙니다. 간단한 경우나, 반환 타입이 인자 타입에 크게 종속되지 않는다면 유니온 타입을 사용하는 것이 더 간결할 수 있습니다.
함수 오버로딩은 하나의 함수가 여러 형태의 동작을 수행할 때 타입 안정성을 확보하면서 코드의 가독성을 높이는 고급 기능입니다. 특히 라이브러리나 복잡한 유틸리티 함수를 작성할 때 그 진가를 발휘합니다. 오버로드 시그니처와 구현 시그니처의 역할을 명확히 이해하고, 타입 가드를 적절히 활용하는 것이 중요합니다.