웹/프론트엔드

[Typescript] 유니온 타입 사용 시 타입 증명하기

이민훈 2022. 11. 23. 04:15

타입스크립트에서 유니온 타입을 유용하게 쓸 일이 되게 많다.

만약 그 유니온 타입이 primitive type끼리 유니온 연산을 한 것이라면 문제는 간단하다.

typeof 연산으로 쉽게 타입을 정제할 수 있다.

 

const func = (value: number | string) => {
  if(typeof value === "string") {
    value.toLowerCase();
  }
};

 

하지만 복잡한 객체의 경우는 다르다.

공통으로 가진 프로퍼티가 아니면 각 객체가 가지는 프로퍼티에 접근이 불가능하다.

이때 타입스크립트에 값이 해당 프로퍼티를 가진 타입이나 인터페이스라는 것을 증명하여야 한다.

 

interface Device {
    price: number;
    powerOn: () => void;
    powerOff: () => void;
}

interface Phone extends Device {
    call: (phoneNumber: string) => void;
}

interface Computer extends Device {
    terminal: () => void;
}

const func = (device: Phone | Computer) => {
    device.call("01012341234");
}

 

예를 들어 위 코드에서 func라는 함수는 Phone | Computer라는 유니온 타입을 파라미터로 넘겨받는다.

price, powerOn, powerOff 등 Phone과 Computer가 동시에 가지는 프로퍼티라면 접근이 가능하지만,

Phone만이 가진 call은 호출할 수 없다. device가 Computer일 가능성도 있기 때문이다.

 

interface Device {
    type: string;
    price: number;
    powerOn: () => void;
    powerOff: () => void;
}

interface Phone extends Device {
    type: "phone";
    call: (phoneNumber: string) => void;
}

interface Computer extends Device {
    type: "computer";
    terminal: () => void;
}

const func = (device: Phone | Computer) => {
    if(device.type === "phone") {
        device.call("01012341234");
    }
}

 

그럴 때 위와 같이 타입을 나타내는 프로퍼티를 추가하여 타입스크립트가 추론하게 할 수 있다.

인터페이스가 아닌 타입의 경우도 다르지 않다.

 

type Device = {
  type: string;
  price: number;
  powerOn: () => void;
  powerOff: () => void;
};

type Phone = {
  type: "phone";
  call: (phoneNumber: string) => void;
} & Device;

type Computer = {
  type: "computer";
  terminal: () => void;
} & Device;

const func = (device: Phone | Computer) => {
  if (device.type === "phone") {
    device.call("01012341234");
  }
};

 

type이라는 프로퍼티를 따로 만드는 것이 싫다면

아래의 경우처럼 해당 프로퍼티가 객체 내에 존재하는지를 검사하는 in 연산자를 쓸 수도 있겠다.

 

interface Device {
    price: number;
    powerOn: () => void;
    powerOff: () => void;
}

interface Phone extends Device {
    call: (phoneNumber: string) => void;
}

interface Computer extends Device {
    terminal: () => void;
}

const func = (device: Phone | Computer) => {
    if("call" in device) {
        device.call("01012341234");
    }
}

 

만들어진 여러 타입을 가진 객체에 키로 접근할 때도 크게 다르지 않다.

다만 해당 경우 tsconfig의 strict 모드를 켜주지 않으면 타입스크립트가 item을 any로 추론하게 되니 주의하자.

if문이 없으면 해당 코드는 런타임 에러를 일으킬 가능성이 높은 코드다.

 

interface Device {
  type: string;
  price: number;
  powerOn: () => void;
  powerOff: () => void;
}

interface Phone extends Device {
  call: (phoneNumber: string) => void;
}

interface Computer extends Device {
  terminal: () => void;
}

interface MyDevices {
  phones: Phone[];
  computers: Computer[];
}

const func = (devices: MyDevices, key: keyof MyDevices) => {
  devices[key].forEach((item) => {
    // if문이 없으면 런타임 에러를 일으킬 수 있는 코드다.
    if ("call" in item) {
      item.call("01012341234");
    }
  });
};

 

마지막으로 클래스의 경우를 보자.

 

class Device {
  price: number;
  constructor(price: number) {
    this.price = price;
  }
  powerOn() {
    console.log("power on");
  }
  powerOff() {
    console.log("power off");
  }
}

class Phone extends Device {
  call(phoneNumber: string) {
    console.log(`call to ${phoneNumber}`);
  }
}

class Computer extends Device {
  terminal() {
    console.log("terminal on");
  }
}

const func = (device: Phone | Computer) => {
  if ("call" in device) {
    device.call("01012341234");
  }
  if (device instanceof Phone) {
    device.call("01012341234");
  }
};

 

클래스의 경우는, 값 수준 연산인 in과, 타입 수준 연산인 instanceof를 모두 사용할 수 있다.

A instanceof B는 A가 B의 인스턴스인지 boolean을 반환하는 연산이다.

 

타입스크립트에서 활용도가 높은 유니온 타입을 사용할 때, 타입스크립트에 증명하거나 직접 추론하게 하기 위한 방법을 알아보았다.

타입스크립트의 장점을 잘 활용하여, 런타임 에러를 컴파일 단계에서 잡도록 해보자.