개발새발

같은 이름의 함수를 여러 번 선언 할 수 있다 본문

Typescript

같은 이름의 함수를 여러 번 선언 할 수 있다

비숑주인 2025. 3. 29. 20:25

TypeScript 함수 오버로딩: 언제, 왜, 어떻게 쓸까?

 

JavaScript에서는 함수의 인자 개수나 타입에 제약이 없다. 하지만 TypeScript에서는 매개변수의 타입과 개수를 명확히 지정해야 하므로, 복잡한 함수 시그니처를 다룰 때 오버로딩(Overloading)이 매우 유용해진다. 

 

오버로딩이 필요한 이유

아래처럼 숫자 덧셈 또는 문자열 결합을 처리하는 함수를 생각해자. 

function add(x: string | number, y: string | number): string | number {
  return x + y;
}

 

이 코드에는 두 가지 문제점이 있다:

  1. + 연산자는 서로 다른 타입 (number + string)에 사용할 수 없기 때문에 컴파일 에러 발생
  2. add(1, '2') 같은 호출도 허용됨 → 의도와 다르게 동작할 수 있음

 

해결책: 함수 오버로딩

function add(x: number, y: number): number;
function add(x: string, y: string): string;
function add(x: any, y: any) {
  return x + y;
}

 

  • 오버로드 시그니처: 위 두 개 (x: number..., x: string...)
  • 구현부: 실제 함수 본문은 any를 사용 (단, any로 선언해도 오버로드된 타입만 허용됨)

 

허용되는 호출

add(1, 2);          // OK → number
add('1', '2');      // OK → string

 

허용되지 않는 호출

add(1, '2');        // ❌ 에러
add('1', 2);        // ❌ 에러

 

에러 메시지에는 Overload 1 of 2, Overload 2 of 2 등이 포함되어, 어떤 시그니처도 일치하지 않음을 알려준다. 

 

오버로딩 순서의 중요성

function example(param: string): string;
function example(param: string | null): number;
function example(param: string | null): string | number {
  return param ? 'string' : 123;
}

const result = example('hello'); // 결과 타입: string

 

오버로딩 시 위에 선언된 시그니처가 우선 매칭 된다. 위 예제에서 'hello'는 string | null에도 해당되지만, string 시그니처가 먼저 매칭된다. 

 

오버로딩 순서를 바꾸면?

function example(param: string | null): number;
function example(param: string): string;
...
const result = example('hello'); // 결과 타입: number ❗️

 

실제 반환값은 string인데 타입은 number가 되어 런타임 에러가 날 수 있으니 좁은 타입을 먼저 선언해야 안전하다.

 

인터페이스/타입 별칭으로도 오버로딩 가능

interface Add {
  (x: number, y: number): number;
  (x: string, y: string): string;
}

const add: Add = (x: any, y: any) => x + y;
type Add1 = (x: number, y: number) => number;
type Add2 = (x: string, y: string) => string;
type Add = Add1 & Add2;

const add: Add = (x: any, y: any) => x + y;

 

둘 다 동일하게 동작하며, add(1, 2)나 add('1', '2')는 OK, add(1, '2')는 안 된다. 

 

오버로딩이 불필요한 경우

아래와 같이 단순히 유니언이나 옵셔널로 표현 가능한 상황에서는 굳이 오버로딩을 사용할 필요가 없다. 

function a(param: string | number) {}
function errorA(param: string | number) {
  a(param); // ✅ 문제 없음
}

function b(p1: string, p2?: number) {}
function errorB(p1: string, p2: number | undefined) {
  b(p1, p2); // ✅ 문제 없음
}

 

반면 아래처럼 오버로딩하면 타입 오류가 발생한다. 

function a(param: string): void;
function a(param: number): void;
function a(param: string | number) {}

function errorA(param: string | number) {
  a(param); // ❌ string | number는 일치하는 오버로드 없음
}

 

유니언 타입을 오버로딩 시그니처에 직접 전달하면 일치하지 않음.

 

언제 오버로딩을 써야 할까?


상황 오버로딩 사용 여부
인자 타입과 반환값이 명확히 2개 이상으로 구분될 때 ✅ 사용 추천
단순히 유니언 또는 옵셔널로 표현 가능할 때 ❌ 사용 지양
다양한 인자 조합을 받지만 내부 구현이 동일할 때 ❌ 조건 분기로 처리 추천
구현부와 오버로드가 정확히 일치할 수 있을 때 ✅ 사용 가능

 

함수 오버로딩은 타입 안정성을 높이고, 개발자의 의도를 명확히 표현할 수 있다. 다만, 무조건적인 사용은 코드 복잡도만 높일 수 있으니, 아래 원칙에 따라 사용하자.

 

  • 가능한 한 유니언/옵셔널로 표현하자
  • 좁은 타입 → 넓은 타입 순으로 오버로딩 선언하자
  • 구현부와 오버로드 시그니처는 항상 일치하도록 하자