ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [TS] Typescript의 메소드 오버로딩
    Javascript, Typescript 2021. 11. 4. 22:41
    반응형

    최근에 유용하게 사용하고 있는 TypeORM 이라는 Typescipt의 ORM 라이브러리는 데이터를 저장하는 함수로 repository 클래스의 save라는 메소드를 제공한다.

    이 save 메소드는 공식문서에서 확인할 수 있는 것처럼, 매개변수로 단일 객체 또는 배열 두 가지를 다 넘겨줄 수 있기 때문에 굉장히 유용하다.

    TypeORM 공식문서

    그리고 VS code 같은 IDE의 Intellisense를 통해, save메소드에 단일 객체를 넘겨주면 단일 객체를 resolve하는 Promise를, 배열을 넘겨주면 배열을 resolve하는 Promise를 반환한다는 것을 알 수 있다.

    단일 객체를 넘겨줄 때
    배열을 넘겨줄 때

    그렇다면 타입스크립트 컴파일러는 어떻게 단일 객체를 넘겨주면 단일 객체를, 배열을 넘겨주면 배열을 반환한다는 것을 알고 있는 걸까?


    save 메소드 구현하기

    email이라는 프로퍼티를 갖는 User 인터페이스와, User를 전달받아서 내부 배열에 추가하는 save 메소드를 가진 UserService 클래스가 다음과 같이 있다.

    interface User {
      email: string;
    }
    class UserService {
      private _users: User[] = [];
    
      save(user: User) {
        this._users.push(user);
        return user;
      }
    }

    saveUser 메소드에 반환 값의 타입을 명시하지 않았지만, 똑똑한 타입스크립트 컴파일러는 이 메소드가 받는 매개변수의 타입을 통해 반환 타입을 알 수 있다. 이 메소드를 사용할 때 VS code의 Intellisense를 통해 이 save 메소드의 매개변수의 타입과 반환 값의 타입을 확인할 수 있다.

    하지만 우리가 구현하고자 하는 함수는 단일 객체와 배열 모두 매개변수로 받을 수 있어야 한다. TypeORM Repository의 save 메소드 처럼, 단일 객체와 배열 모두 매개변수로 받을 수 있게 save 메소드를 수정해보자.

    class UserService {
      private _users: User[] = [];
    
      save(user: User | User[]) {
        if (Array.isArray(user)) {
          this._users.push(...user);
        } else {
          this._users.push(user);
        }
        return user;
      }
    }

    이제 save메소드에 단일 객체와 배열 모두 넘겨줄 수 있게 되었다. 그런데 이제는 타입스크립트 컴파일러가 매개변수의 타입과 리턴 타입을 매치해서 알려주지 못하는 것을 확인할 수 있다.

    단일 객체와 배열을 매개변수로 넘겨줄 수 있다는 것만 알 수 있지, 매개변수로 User의 배열을 넘겨주면 User의 배열을 반환한다는 것은 알 수가 없다. 타입스크립트 컴파일러가 매개변수로 받을 수 있는 두 개의 타입을 그대로 반환 타입으로 알려줄 뿐이다.

    save 메소드에 단일 객체를 넣었을 때는 단일 객체를, 배열을 넣었을 때는 배열을 리턴한다는 것을 TypeORM의 save메소드처럼 컴파일러가 알 수는 없을까? 이때 타입스크립트의 메소드 오버로딩이 필요하다.

    Method Overloading

    In some programming languages, function overloading or method overloading is the ability to create multiple functions of the same name with different implementations. — Wikipedia

    자바스크립트의 함수는 오버로딩을 지원하지 않지만 타입스크립트는 메소드 오버로딩을 지원한다.

    다만 다른 언어처럼 매개변수의 수와 타입이 다른 메소드를 여러 개 구현할 수 있는 것과는 달리, 매개변수의 타입이 다른 함수 여러 개를 선언만 할 수 있고 실제 구현은 한 번만 할 수 있다. 매개변수의 수는 달라지면 안된다.

    class UserService {
      private _users: User[] = [];
      
      save(user: User): User;       // 선언 1
      save(user: User[]): User[];   // 선언 2
      save(user: User | User[]) {   // 구현
        if (Array.isArray(user)) {
          this._users.push(...user);
          return user;
        }
        this._users.push(user);
        return user;
      }
    }

    기존에 구현했던 save 메소드에는 바뀐 것이 없다. 선언 1에 매개변수와 반환 타입이 User인 함수를 선언했고, 선언 2에서 매개변수와 반환 타입이 User의 배열인 함수를 선언했을 뿐이다. 기존의 코드가 앞서 선언한 두 함수 선언의 구현이었다고 볼 수 있다.

    타입스크립트 컴파일러가 두 가지 매개변수의 타입과 각 타입에 해당하는 반환 타입을 정확히 알게 되었다. User의 배열을 넘겨줬으니 User의 배열을 반환한다는 것과 이 메소드에 오버로드가 추가로 있다는 것 또한 intellisense를 통해 알 수 있다.


    위 예제 코드에 있는 두 개의 함수 선언을 타입스크립트의 제네릭(Generic)조건문을 사용하면, 다음처럼 하나의 함수 선언으로 멋지게 작성할 수도 있다.

    class UserService {
      private _users: User[] = [];
    
      save<T extends User | User[]>(user: T): T extends User ? User : User[];
      save(user: User | User[]) {
        if (Array.isArray(user)) {
          this._users.push(...user);
          return user;
        }
        this._users.push(user);
        return user;
      }
    }

    우리가 라이브러리를 사용할 때 IntelliSense가 친절하게 매개변수, 리턴 타입 등 사용방법을 알려줄 수 있는 것도, 해당 라이브러리의 .d.ts 파일에 이렇게 메소드 오버로딩 통해 타입 유추를 도와줄 수 있는 선언 코드들이 있기 때문이다.

    TypeORM의 Repository.d.ts 파일 중

    Conclusion

    자바스크립트를 이용한 코드 작성 또는 협업을 할 때 가장 힘든 부분은 다른 사람이 만든 함수를 사용할 때 해당 함수의 매개변수의 타입과 리턴 타입을 유추할 수 없다는 점이다.(내가 작성했던 코드일지라도...)

    타입스크립트를 사용하면서 이러한 불편함이 사라지게 되었지만 메소드 오버로딩같은 기법이 없이는 여전히 정확한 타입 유추가 불가능하기 때문에, 타입스크립트의 장점을 십분 활용하기 위한 메소드 오버로딩의 필요성과 중요성을 느낄 수 있게 되었다.

    반응형

    댓글

Designed by Tistory.