오버로드된 함수 타입
호출 시그니처에서 사용한 함수 타입 문법(type Fn = (...) => ...)은 단축형 호출 시그니처(shorthand call signature)입니다.
해당 호출 시그니처를 더욱 명확하게 표현할 수 있습니다.
다시 Log를 예로 살펴보겠습니다.
// 단축형 호출 시그니처
type Log1 = (message: string, userName?: string) => void
// 전체 호출 시그니처
type Log2 = {
(message: string, userName?: string): void
}
위의 두 코드는 문법만 조금 다를 뿐 모든 면에서 같습니다.
위의 Log 함수처럼 간단한 상황이라면 단축형을 활용하되 더 복잡한 함수라면 전체 시그니처를 사용하는 것이 좋을 때도 있습니다.
이것이 바로 함수 타입의 오버로딩(overloading)이 좋은 예입니다.
오버로드된 함수
호출 시그니처가 여러 개인 함수
대부분의 프로그래밍 언어에선 여러 매개변수를 인수로 받아 어떤 타입의 값을 반환하는 함수를 선언한 다음, 이 함수가 요구하는 정확한 매개변수 집합을 건네서 함수를 호출하면, 항상 똑같은 타입의 반환값을 받게 됩니다.
그러나 JavaScript는 예외입니다.
JavaScript는 동적 언어이므로 어떤 함수를 호출하는 방법이 여러 가지이며, 인수 입력 타입에 따라 반환 타입이 달라질 때도 있습니다.
TypeScript는 이러한 동적 특징을 오버로드된 함수 선언으로 제공하고, 입력 타입에 따라 달라지는 함수의 출력 타입은 정적 타입 시스템으로 각각 제공합니다.
이런 언어 기능은 타입 시스템의 고급 기능에 속합니다.
오버로드된 함수 시그니처를 이용하면 표현력 높은 API를 설계할 수 있습니다.
예시로 Book이라는 예약 API를 설계한다고 가정해보겠습니다.
먼저 타입을 전체 타입 시그니처를 사용하여 다음처럼 지정할 수 있습니다.
type Book = {
(from: Date, to: Date, destination: string): Book
}
이후 Book의 구현 코드를 봅니다.
let book: Book = (from, to, destination) => {
// ...
}
신라 호텔 숙박 예약하려는 고객이 있다면 from과 to에는 날짜를, destination은 "The Shilla Seoul"로 설정해 book API를 이용할 것입니다.
아래처럼 신라 호텔 레스토랑 이용만을 지원하도록 API를 개선할 수 있습니다.
type Book = {
(from: Date, to: Date, destination: string): Book
(from: Date, destination: string): Book
}
해당 코드를 실행하려 시도하면 TypeScript가 Book을 구현한 코드에서 에러를 발생시킵니다.
이는 TypeScript가 호출 시그니처 오버로딩을 처리하는 방식 때문에 발생합니다.
함수 f에 여러 개의 오버로드 시그니처를 선언하면, 호출자 관점에서 f의 타입은 이들의 유니온이 됩니다.
하지만 f를 구현하는 관점에선 단일 구현으로 조합된 타입을 나타낼 수 있어야 합니다.
조합된 시그니처는 자동으로 추론되지 않으므로 f를 구현할 때 직접 선언해야 합니다.
Book 예제에선 book 함수를 아래처럼 바꿀 수 있습니다.
type Book = {
(from: Date, to: Date, destination: string): Book
(from: Date, destination: string): Book
} // ①
let book: Book = (
from: Date,
toOrDestination: Date | string,
destination?: string) => { // ②
// ...
}
① 오버로드된 함수 시그니처 두 개를 선언합니다.
② 구현의 시그니처는 두 개의 오버로드 시그니처를 수동으로 결합한 결과와 같습니다.(즉, Signature1 | Signature2를 손으로 계산한 것과 같음) 결합된 시그니처는 book을 호출하는 함수엔 보이지 않습니다.
다음은 소비자 관점의 Book 시그니처입니다.
type Book = {
(from: Date, to: Date, destination: string): Book
(from: Date, destination: string): Book
}
결과적으로 이전에 정의한 결합된 시그니처를 모두 포함하지 않습니다.
// 잘못됨!
type Book = {
(from: Date, to: Date, destination: string): Book
(from: Date, destination: string): Book
(from: Date, toOrDestination: Date | string, destination?: string): Book
}
두 가지 방식으로 book을 호출할 수 있으므로 book을 구현할 때 TypeScript에 book이 어떤 방식으로 호출되는지 확인시켜 줘야 합니다.
let book: Book = (
from: Date,
toOrDestination: Date | string,
destination?: string) => {
if (toOrDestination instanceof Date && destination !== undefined){
// 레스토랑 이용 예약
}
else if (typeof toOrDestination === 'string'){
// 숙박 예약
}
}
오버로드 시그니처는 구체적으로 유지
오버로드된 함수 타입을 선언할 때는 각 오버로드 시그니처(Book)를 구현 시그니처(book)에 할당할 수 있어야 합니다.
즉, 오버로드를 할당할 수 있는 범위에서 구현의 시그니처를 얼마든지 일반화할 수 있습니다.
let book: Book = (
from: any,
toOrDestination: any,
destination?: any) => {
// ...
}
오버로드를 사용할 땐 함수를 쉽게 구현할 수 있도록 가능한 구현 시그니처를 특정하는 것이 좋습니다. 때문에 any 대신 Date, any 대신 Date | String 유니온을 사용하는 것입니다.
만약 any 타입으로 받은 매개변수를 Date로 사용하고자 한다면 먼저 그 값이 실제로 날짜임을 TypeScript에 증명해야 합니다. 그래야 자동완성 기능 혜택을 볼 수 있기 때문입니다.
function getMonth(date: any): number | undefined {
if (date instanceof Date) {
return date.getMonth()
}
}
반면에 Date 타입임을 미리 명시해두면 구현 시 할 일이 줄어듭니다.
function getMonth(date: Date): number {
return date.getMonth()
}
'👶 TypeScript' 카테고리의 다른 글
언제 제네릭 타입이 한정되는가? (0) | 2023.01.12 |
---|---|
다형성 (0) | 2023.01.12 |
문맥적 타입화 (0) | 2023.01.12 |
호출 시그니처 (0) | 2023.01.12 |
반복자 (0) | 2023.01.11 |