본문 바로가기

javascript/typescript

[타입스크립트] 함수 오버로딩

 타입스크립트는 정적 타이핑을 사용할 수 있다. 이때 자바, C++, C# 등 정적 타이핑을 사용하는 다양한 언어들에서는 함수 오버로딩 기법을 지원하고 있는데, 타입스크립트 역시 명색이 정적 타이핑 언어이므로 이것이 가능하다.

 문제는 타입스크립트가 자바스크립트로부터 완전히 독립된 형태로 동작하지 않는다는 점이다. 타입스크립트는 자바스크립트에 문법이나 특정 기능을 추가하여 에디터 수준에서 동적 타이핑을 지원하기는 하지만, 실제 프로그램으로 동작할 때는 자바스크립트 코드로 치환된다. 따라서 비록 타입스크립트 문법을 이용하여 코드를 작성하더라도 해당 코드가 자바스크립트의 규칙을 깨서는 안된다. 적어도 이 특징은 타입스크립트가 자바스크립트로 치환되지 않고도 사용 환경에서 사용될 수 있는 기술이 등장하지 않는 한 변할 일이 없을 것이다.

 그렇다면 자바스크립트에서는 함수 오버로딩이 될까? 자바스크립트는 동적 타이핑 언어다. 따라서 함수 오버로딩의 개념 자체가 성립하지 않는다. 함수 오버로딩이라는 개념은 함수의 파라미터 타입과 반환 타입이 컴파일 시점에 구분되어야 성립하는데, 자바스크립트는 컴파일 타임에 타입이 존재하지 않기 때문이다. 만약 자바스크립트에서 오버로딩을 구현하고 싶다면, 하나의 함수 안에서 조건문과 typeof, instanceof 등 타입을 narrowing할 수 있는 키워드 등을 이용하여 각 조건에 따라 다른 동작을 수행하도록 구현해야 한다. 물론 전통적인 오버로딩 방식과는 거리가 멀다.

 위와 같은 자바스크립트의 특징에 따라 타입스크립트에서의 오버로딩은 다른 정적 언어와는 다른 특징을 가진다. 이때 타입스크립트에서는 오버로딩의 선언을 오버로딩 시그니처, 실제 구현을 구현 시그니처로 칭한다.

  1. 오버로딩 시그니처 및 구현 시그니처가 구분되어 있다.
  2. 구현 시그니처에서는 오버로딩 시그니처 각각에 대한 로직을 구현하며, 하나만 존재한다.
interface IdLabel {
    id: number;
}

interface NameLabel {
    name: string;
}

function createLabel(name: string): NameLabel;
function createLabel(id: number): IdLabel;
// function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
    switch (typeof nameOrId) {
        case 'number':
            return {
                id: nameOrId
            } as IdLabel;
        case 'string':
            return {
                name: nameOrId
            } as NameLabel;
    }
    throw new Error("cannot reach here!");
}

 

 위 코드에서는 createLabel 함수에 대해 2개의 오버로딩 시그니처가 존재한다. 구현 시그니처의 파라미터 및 반환 타입은 선언된 오버로딩들의 타입의 union으로 구성되며, 내부에서는 각각의 오버로딩 시그니처에 맞는 로직을 구현하고 있다. 결과적으로 createLabel 함수는 문자열이 들어오면 NameLabel 타입의 객체를, 숫자가 들어오면 IdLabel 타입의 객체를 반환하게 된다.

문자열을 넘겼더니, NameLabel 타입을 반환한다.

 

https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#inferring-within-conditional-types

타입스크립트의 기능 중 하나인 조건부 유형 ( conditional type ) 및 제네릭을 이용하여 함수 오버로딩을 구현할 수도 있다. 개발환경에서 함수 오버로딩으로 인식하지 않는다는 단점이 있지만, 각 오버로딩의 파라미터와 반환 타입을 좀더 강하게 제약할 수 있고, 오버로딩 시그니처가 따로 필요하지 않다는 장점도 존재한다.

아래 예시 코드에서는 동물 타입을 구현하고, 해당 타입들에 대해 조건부 유형 방식을 적용하여 ( T extends 부분 ) animalCrying 함수에 대한 오버로딩을 구현하고 있다. 오버로딩 시그니처가 추가되지 않는게 특징이다.

abstract class Animal {
    protected name: string;

    constructor(name: string) {
        this.name = name;
    }

    public abstract cry(): void;
}

class Cat extends Animal {
    public cry(): void {
        console.log(`[${this.name}] : 야옹!`);
    }

    public Meow() {
        console.log("냐옹이다옹");
    }
}
class Dog extends Animal {
    public cry(): void {
        console.log(`[${this.name}] : 멍멍!`);
    }

    public BowWow() {
        console.log("그르릉");
    }
}
class Bird extends Animal {
    public cry(): void {
        console.log(`[${this.name}] : 짹짹!`);
    }


    public Twit() {
        console.log("짹째잭");
    }
}
// 동물들에 대한 클래스 구현

type AnimalType = "cat" | "dog" | "bird";
// 파라미터 타입
type AnimalReturnType<T extends AnimalType> =
    T extends "cat"
    ? Cat
    : T extends "dog"
    ? Dog
    : Bird;
// 반환 타입

function animalCrying<T extends AnimalType>(animal: T, name: string): AnimalReturnType<T> {
    switch (animal)
    {
        case "dog":
            const dog = new Dog(name) as AnimalReturnType<T>;
            dog.cry();
            return dog;
        case "cat":
            const cat = new Cat(name) as AnimalReturnType<T>;
            cat.cry();
            return cat;
        case "bird":
            const bird = new Bird(name) as AnimalReturnType<T>;
            bird.cry();
            return bird;
    }
    throw new Error("Cannot reach here!");
}
// 오버로딩 함수

const a = animalCrying("dog","hoho"); // a : Dog
const b = animalCrying("cat", "mal"); // b : Cat
const c = animalCrying("bird", "wal");// c : Bird

animal로 "dog"를 넘기면, Dog 객체를 반환한다.

오버로딩 시그니처가 매우 많이 존재하는 상황이라면 이 방법도 나쁘지는 않아보인다. 개인적으로는 더 복잡한 것 같다.

 

결론

아직까지 타입스크립트에서 함수 오버로딩을 일반적인 정적 타이핑 언어처럼 수행할 수는 없다. 대신 오버로딩 시그니처 및 구현 시그니처를 구분하여 구현함으로써 자바스크립트로의 변환 과정을 고려한 오버로딩은 구현할 수 있다.