런타임에 타입 검사를 하는 법

hejuby·2024년 5월 1일
0

개요

타입스크립트의 특성 상 타입 체커를 통과한 코드를 포함한 모든 타입스크립트 코드는 결국 자바스크립트로 변환되어 실행됩니다. 이때 모든 인터페이스와 타입이 제거되기 때문에 코드가 예상과 맞지 않게 동작할 때가 있습니다. 타입스크립트 타입 대신 사용할 수 있는 별도의 방법을 이펙티브 타입스크립트에 실린 예제를 중심으로 알아봅니다.

런타임 타입 검사의 필요성

상술했던 대로 타입스크립트 코드는 런타임에 모든 타입이 제거됩니다. 따라서 특정 상황의 경우 선언된 타입과 런타임 시 실제 타입이 일치하지 않을 수 있습니다. 예를 들어 타입 단언을 사용하는 경우, 외부 API를 이용하는 경우, 또는 구조적 타이핑에 기반해 타입 체커를 통과하는 경우 등이 있습니다. 이를 방지하기 위해 런타임에 타입 검사를 하는 일이 필요합니다.

제대로 동작하지 않는 타입 검사

interface Square {
  width: number;
}

interface Rectangle extends Square {
  height: number;
}

type Shape = Square | Rectangle;

function getArea(shape: Shape) {
  // 'Rectangle' only refers to a type, but is being used as a value here.(2693)
  if (shape instanceof Rectangle) {
    // Property 'height' does not exist on type 'Shape'.
  	// Property 'height' does not exist on type 'Square'.(2339)
    return shape.width * shape.height;
  } else {
    return shape.width * shape.width;
  }
}

위의 코드는 타입스크립트에서 오류를 내뿜을 뿐더러 자바스크립트로 변환해 실행되도 제대로 타입 검사가 이루어지지 않습니다. 그 이유는 instanceof 연산자의 매개변수로 값이 아닌 타입스크립트의 타입을 전달하고 있기 때문입니다. 이를 해결하기 위해 책에서는 세가지 방법을 제시합니다.

in 연산자를 이용한 속성 체크

in 연산자를 사용하면 객체가 특정 속성을 가지고 있는지 체크할 수 있습니다. 위에서 봤던 예제의 경우, height 속성의 보유 여부를 검사하면 Shape 객체가 Square인지 Rectangle인지 판별할 수 있게 됩니다. 따라서 getArea 함수를 다음과 같이 재작성할 수 있습니다.

function getArea(shape: Shape) {
  if ("height" in shape) {
    return shape.width * shape.height;
  } else {
    return shape.width * shape.width;
  }
}

태그 기법 사용

태그 기법이란, 런타임에 접근 가능한 타입 정보를 명시적으로 저장하는 것을 말합니다.

interface Square {
  _type: "square";
  width: number;
}

interface Rectangle {
  _type: "rectangle";
  width: number;
  height: number;
}
...
function getArea(shape: Shape) {
  if (shape._type === "rectangle") {
...

리터럴 타입을 사용해 타입 정보를 스트링 리터럴로 명시해 준 것을 볼 수 있습니다. 이 경우 Square 인터페이스를 그대로 상속받아 Rectangle 인터페이스를 선언하는 것은 불가능하며, 별개로 Rectangle 인터페이스를 선언하거나 Pick, Omit 등을 이용해 리터럴 속성을 제외한 속성만을 상속받아 타입을 구현할 수 있습니다.

타입을 클래스로 선언

타입을 클래스로 선언하면 해당 타입을 타입뿐만 아니라 값으로 이용할 수 있습니다. 따라서 런타임에 instanceof 연산자가 문제 없이 동작하게 됩니다.

class Square {
  constructor(public width: number) {}
}

class Rectangle extends Square {
  constructor(public width: number, public height: number) {
    super(width);
  }
}
...
function getArea(shape: Shape) {
  if (shape instanceof Rectangle) {
...

느낀 점

그 동안 instanceof 연산자 또는 is 키워드(type predicate)만을 사용해 타입 검사를 해왔습니다. 그래서 런타입에 타입을 검사할 필요가 있을 때 이를 구현하는 방법이 막막했는데 새로운 인사이트를 얻을 수 있었습니다. 클래스를 활용하는 방법은 해당 클래스 객체를 이용할 일이 있을 때만 사용할 것 같고, 주로 in 연산자나 태그 기법을 활용할 것 같습니다.

참고문헌

이펙티브 타입스크립트

0개의 댓글