느릿늘있

[TypeScript] 오버로딩(Overloading) 본문

개발공부

[TypeScript] 오버로딩(Overloading)

JHKim93 2023. 7. 23. 23:11

  타입스크립트를 공부하면서 오버로딩이 조금 특이하게 동작하는 부분이 있는 것 같아서 정리 겸 공유를 위해 기록을 남겨본다. 우선, 자바스크립트는 함수의 매개변수와 리턴타입에 관대하기 때문에 오버로딩이 필요가 없다. 

function a (arg) { return arg }

// 에러가 발생하지 않는다.
a(true, 1) // true
a("12", 123, true) // "12"
a() // undefined

  타입스크립트에서 타입을 설정하지 않으면 자바스크립트와 동일하게 동작할까?

function a (arg){
    console.log(arg)
    console.log(typeof(arg))
}

// 매개변수 갯수에 대해 엄격하게 체크한다.
a(1, '2') // error : Expected 1 arg, but got 2
a() // error : Expected 1 arg, but got 0

// arg는 default로 any가 설정되며 타입 추론이 동작한다.
a(1) // 1 number
a('2') // 2 string

  이처럼 자바스크립트와는 다르게 타입스크립트는 함수의 매개변수에 대해 타입과 갯수를 체크하는 것으로 보인다. 따라서 다형성을 제공하기 위한 오버로딩 기능을 제공할 것으로 예측할 수 있다. 실제로 타입스크립트에서 함수의 오버로딩 기능을 제공하는 방식은 아래와 같다.

// 1. 매개변수 개수에 대한 오버로딩
function a (arg): void
function a (arg, arg2): void

// optional property 활용
function a (arg, arg2?){
    console.log(arg)
    console.log(typeof(arg))
    console.log(arg2)
    console.log(typeof(arg2))
}

a(1) // 1 number undefined undefined
a(1, '2') // 1 number 2 string
a() // error : Expected 1-2 arguments, but got 0.
a(1,2,3) // error : Expected 1-2 arguments, but got 3.
// 2. 매개변수 타입에 대한 오버로딩
function a (arg:string): void
function a (arg:number): void

function a (arg: any){
    console.log(arg)
    console.log(typeof(arg))
}

a(1) // 1 number
a('2') // 2 string
a(true)
// error : Argument of type 'boolean' is not assignable to parameter of type 'string'.
// error : Argument of type 'boolean' is not assignable to parameter of type 'number'.

  두 가지를 혼합해서 개수와 타입에 대해 동시에 오버로딩하도록 할 수도 있다. 그런데 타입스크립트를 조금 써 본 사람들은 뭔가 이상하다고 느낄 것이다. 사실 optional property는 저렇게 굳이 선언부와 구현부를 나누지 않아도 잘 동작한다. 타입에 대한 오버로딩도 저렇게 any를 쓰는 것보다 차라리 선언부 없이 유니온 타입으로 사용하는 것이 더 좋아 보인다. (무분별한 any 타입 사용 지양)

function a (arg: string | number){
    console.log(arg)
    console.log(typeof(arg))
}

  마지막 세번째로 오버로딩은 반환값이 여러 타입일 때는 필요해 보인다. 이펙티브 타입스크립트(댄 밴더캄 저)를 보면 " 사용할 때는 너그럽게 생성할 때는 엄격하게 "라는 타입스크립트 사용 원칙을 제안한다. 쉽게 말해서 함수의 매개변수는 너그럽게 받더라도 반환값은 엄격하게 제한해야 한다는 의미이다. 그러나 실제 개발 시 어쩔 수 없이 하나의 함수가 여러 타입의 값을 반환해야 하는 경우가 있다. 이 때, 오버로딩을 활용하여 특정 상황에서 특정한 값만 반환하도록 엄격하게 제한할 수 있다.

// user의 정보(info)객체 혹은 객체 리스트를 받아서 User 객체 혹은 User 객체의 배열을 반환하는 함수 makeUsers가 있다.
// 단일 객체를 받았을 때는 단일 객체를 반환하고 배열로 받았을 때는 배열로 반환하고 싶다.
interface Info {}
interface User {}

declare function setUser(info: Info): User

function makeUsers(info: any): any {
  if (Array.isArray(info)) {
    const userList = info.map((data: Info) => setUser(data))
    return userList
  } else {
      const user = setUser(info)
      return user
  }
}
let info: Info = {}
let infoList: Info[] = [{}, {}]

// 단일객체를 매개변수로 넣든 객체 배열을 넣든 동일한 유니온 타입 반환값을 보여준다.
const a = makeUsers(info) // User | User[]
const b = makeUsers(infoList) // User | User[]
// 오버로딩을 활용하여 정확한 설계 내용을 보여줄 수 있다.
interface Info {}
interface User {}

declare function setUser(info: Info): User

// 아래 오버로딩 함수의 순서를 바꾸면 제대로 동작하지 않는데
// 범위가 큰 함수가 먼저 선언되어야 하는 것 같다.(정확하지 않음)
function makeUsers(info: Info[]): User[]
function makeUsers(info: Info): User

function makeUsers(info: any): any {
  if (Array.isArray(info)) {
    const userList = info.map((data: Info) => setUser(data))
    return userList
  } else {
      const user = setUser(info)
      return user
  }
}
let info: Info = {};
let infoList: Info[] = [{}, {}];

// 단일 객체를 넣는 경우와 리스트 객체를 넣는 경우 추론되는 반환값이 다르다.
const a = makeUsers(info) // User
const b = makeUsers(infoList) // User[]

  단순한 예제들로 타입스크립트의 오버로딩 기능을 살펴 보았다. 실제 개발 환경에서는 훨씬 더 복잡한 상황들이 많이 펼쳐지겠지만 위 내용들이 적절한 트레이드 오프를 선택하고 판단하는 데 도움이 됐기를 바란다.