타입스크립트의 모든 다형성

타입스크립트의 함수 오버로딩에 대해 글을 쓰다가 프로그래밍 이론에서 다형성의 세 가지 주요 유형을 우연히 알게 되었습니다. 이 글에서는 타입스크립트에서의 이 세 가지 유형의 다형성을 간단한 예제들로 소개하고자 합니다.
파라미터 다형성
타입스크립트에서 제네릭 함수를 사용한다면 이 패턴을 모른 채 사용했을 수도 있습니다. 별 기능이 없는 아래 함수를 살펴보세요.
function arrayEntries<T>(items: Array<T>): Array<[number, T]> {
return items.map((item, index) => [index, item]);
}
함수 arrayEntries는 선언 중에 입력될 타입을 알 수 없으므로 "제네릭"이라고 불립니다. 함수가 호출되면 반환될 데이터 타입을 추론할 것입니다.
const entries = arrayEntries(['one', 'two']); // [[0, 'one'], [1, 'two']]
위 예제에서 변수 entries는 인자에 문자열이 들어오므로 Array<[number, string]> 타입을 받습니다.
"제네릭 함수"는 파라미터 다형성 함수의 다른 이름일 뿐입니다. 따라서 Array<T>와 같은 제네릭 타입도 파라미터 다형성의 한 형태라고 생각합니다만, 제 말을 그대로 받아들이지는 마세요.
서브타입 다형성
서브타입 다형성은 클래스에 의존하기 때문에 타입스크립트에서는 덜 일반적입니다.
interface Pet {
speak: () => void;
}
class Dog implements Pet {
speak() {
return 'woof woof!';
}
}
class Cat implements Pet {
speak() {
return 'meow!';
}
}
위 예제에서 Dog와 Cat은 Pet의 서브타입입니다. Pet의 모든 서브타입을 허용하는 함수 listen을 만들 수도 있습니다.
function listen(pet: Pet): void {
console.log(pet.speak());
}
이를 서브타입 다형성이라고 합니다. speak 메서드는 Pet의 서브타입인 클래스 모두가 사용할 수 있고 클래스마다 고유한 동작을 수행할 수 있습니다.
애드혹 다형성
애드혹 다형성을 저는 "다형성 함수"라고 부릅니다. 더 정확히 말하면 애드혹은 Christopher Strachey가 함수 오버로딩이라고도 하는 이 개념을 분류하기 위해 사용한 용어입니다.
애드혹 다형성은 인자에 따라 동작이 달라진다는 점에서 파라미터 다형성 및 서브타입 다형성과 다릅니다. 즉, 인자의 타입에 따라 특정 구현이 호출됩니다.
C++에서의 함수 오버로딩
위키피디아에서 가져온 아래의 C++ 예제는 동일한 함수 Volume에 대해 서로 다른 구현 (따라서 서로 다른 동작)이 공존하는 것을 보여 줍니다.
int Volume(int s) { // 정육면체의 부피.
return s * s * s;
}
double Volume(double r, int h) { // 원기둥의 부피.
return 3.1415926 * r * r * static_cast<double>(h);
}
long Volume(long l, int b, int h) { // 직육면체의 부피.
return l * b * h;
}
int main() {
std::cout << Volume(10);
std::cout << Volume(2.5, 8);
std::cout << Volume(100l, 75, 15);
}
타입스크립트에서의 함수 오버로딩
타입스크립트에서는 접근 방식이 조금 다릅니다. 동일한 예제를 사용하여 함수 Volume의 각 동작에 대해 (오버로드 시그니처라고도 하는) 하나의 선언을 작성하는 것으로 시작합니다. 저는 보통 각 선언에 JSDoc 주석을 포함시킵니다.
/**
* 정육면체의 부피를 계산합니다.
* @param {number} s - 정육면체의 변.
* @returns {number} 정육면체의 부피.
*/
function Volume(s: number): number;
/**
* 원기둥의 부피를 계산합니다.
* @param {number} r - 원기둥 바닥의 반지름.
* @param {number} h - 원기둥의 높이.
* @returns {number} 원기둥의 부피.
*/
function Volume(r: number, h: number): number;
/**
* 직육면체의 부피를 계산합니다.
* @param {number} l - 직육면체의 세로.
* @param {number} w - 직육면체의 가로.
* @param {number} h - 직육면체의 높이.
* @returns {number} 직육면체의 부피.
*/
function Volume(l: number, h: number, b: number): number;
그런 다음 함수의 실제 구현을 선언합니다.
function Volume(d: number, h?: number, b?: number): number {
if (h !== undefined && b !== undefined) {
return d * h * b;
}
if (h !== undefined) {
return Math.PI * d * d * h;
}
return d * d * d;
}
C++와 다르게 하나의 구현이 함수 Volume의 다양한 동작을 책임집니다. 여기서 제공된 인자 수에 따라 if 문으로 함수의 동작을 조건부로 지정합니다.
console.log(Volume(10)); // 정육면체의 부피를 보여 줍니다.
console.log(Volume(2.5, 8)); // 원기둥의 부피를 보여 줍니다.
console.log(Volume(100, 75, 15)); // 직육면체의 부피를 보여 줍니다.
다형성이 아닌 것
위 예제에서 단일 구현이 여러 선언과 함께 사용되는 것을 보았습니다. 이는 각 동작에 고유한 구현이 있는 다른 언어와는 매우 다릅니다. 그렇다면 이를 정말 애드혹 다형성이라고 할 수 있을까요?
이 질문에 대한 명확한 답을 드릴 수는 없지만 타입스크립트에서 함수 오버로딩 방식을 사용할 때의 두 가지 장점을 강조하고 싶습니다.
이전 예제에서 함수 오버로딩은 함수 Volume의 사용법을 제한합니다. 아래와 같이 함수를 호출할 수 없습니다.
Volume(100, undefined, 15); // 이는 성립하지 않습니다.
이는 함수의 단일 구현만으로 가능할 수도 있지만 오버로드 시그니처가 이를 방지합니다. 또한 JSDoc 주석 덕분에 함수를 사용하는 모든 방법이 IDE에 명확하게 표시되어 있습니다.

함수 오버로딩이 타입스크립트에서 어떻게 구현되든 코드 품질을 개선하고 오류를 방지할 수 있다고 생각합니다.
요약
지금까지 타입스크립트에서 매개변수 다형성, 서브타입 다형성, 애드혹 다형성이 어떻게 구현되는지와 함수 오버로딩이 다른 프로그래밍 언어와 무엇이 다른지 살펴보았습니다. 다음 글에서는 타입스크립트에서 함수 오버로딩의 한계와 다형성 함수를 구현하기 위해 가능한 몇 가지 대안에 대해 자세히 살펴보겠습니다.




