타입 가드

타입스크립트를 사용하다 보면 커스텀한 타입을 만들고, 그것들을 조합해 하나의 유니온 타입으로 만들어 사용하는 경우가 많다. 그런데 그 다음에 어떻게 타입을 좁혀야 할지(type narrow)를 몰라 타입 캐스팅을 해버리는 경우를 코드 리뷰를 하다보면 종종 볼 수 있다. 어떻게 이를 해결할 수 있을까?

기본적인 타입 가드 방법

typeof

타입 축소를 하는 가장 기본적인 방법이다. 단, 런타임에 값에 유형에 대한 가장 기본적인 정보(타입스크립트 내장 타입)만 제공할 수 있다.

"string:", "number", "bigint", "boolean", "symbol", "undefined", "object", "function"

function print(char: string | string[] | null) {
    if(typeof char === "object") {
        return '배열'
    }

    if(typeof char === "string") {
        return '문자'
    }

    return '없어요..'
}

console.log(print()); // 없어요..
console.log(print([])); // 배열
console.log(print('')); // 문자

기본적인 값에 대해서만 반환하여 타입 축소를 하기 때문에 우리가 원하는 사용자가 만든 커스텀한 타입을 비교할 수 없다는 단점이 있다.

instanceof

typeof나 유사하나 커스텀한 타입에 대해 타입 축소를 진행할 때, 타입 가드의 방법으로 사용된다.

class Person {
  age: number;
  name: string;

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

function isPerson(person: object) {
  return person instanceof Person;
}

const jacob = new Person("jacob", 30);

console.log(isPerson({name: "개구리", age: 12})); // false
console.log(isPerson(jacob)); // true

프로퍼티 값이 명확한 경우

프로퍼티에 따라 값을 구분할 수 있는 경우(ex. 서버에서 데이터에 명확한 enum 타입을 보내주는 경우)에 프로퍼티의 값을 비교함으로서 타입 축소를 할 수 있다.

interface IFish {
    type: 'FISH'
    swim: () => void;
}

interface IBird {
    type: 'BIRD'
    fly: () => void;
}

type TAnimal = IFish | IBird;

function moveAnimal(animal: TAnimal) {
    if(animal.type === "FISH") {
        animal.swim()
    }
    if(animal.type === "BIRD") {
        animal.fly();
    }
}

프로퍼티 값이 명확하지 않는 경우

경우에 따라 필드의 유무 만이 유일한 구분 값인 경우도 있다. 예를 들면 아래와 같이 인터페이스가 정의된 경우이다.

interface IFish {
    name: string;
    description: string;
    swim: () => void;
}

interface IBird {
    name: string;
    description: string;
}

type TAnimal = IFish | IBird;

swim이라는 프로퍼티가 있는 경우에만 IFish 타입인거고, swim()라는 메서드를 호출해야 한다. 위의 예제와는 달리 명확하게 데이터를 구분할 기준이 없다.

이럴때는 in 키워드를 통해 타입 범위를 축소하면 된다.

function moveAnimal(animal: TAnimal) {
    if('swim' in animal) {
        animal.swim() // Good!
    }
    animal.swim(); // Errors! Property 'swim' does not exist on type 'TAnimal'.
}

타입 가드의 다른 방법들

is

타입 축소를 통해 타입을 좁혔지만, 기존 타입을 그대로 사용하고 싶은 경우가 있거나 내가 원하던 타입 가드가 제대로 안 이루어졌을 수도 있다. 위쪽에서 봤었던 동물 예제에서 isFish() 함수를 추가해 보자.

interface IFish {
  type: 'FISH'
  swim: () => void;
}

interface IBird {
  type: 'BIRD'
  fly: () => void;
}

type TAnimal = IFish | IBird;

function isFish(animal: TAnimal) {
  return (animal as IFish).swim !== undefined
}

만약 moveAnimal 함수를 isFish()로 리펙토링한다면 과연 잘 동작할까?

분명 코드의 논리만 봐서는 동작해야 할거 같은데, 빨간 밑줄이 생겼고 해당 메서드가 없다는 말이 나온다. 이런 경우 함수 의 반환 유형에 type predicate를 사용하면 되며, parameterName is Type의 형식으로 사용하면 된다. parameterName의 경우 매개 변수의 이름을 사용해야한다.

type predicate를 사용하면 해당 함수가 어떤 변수와 함께 호출될 때마다 타입스크립트에서 원래 유형이 호환되는 경우 해당 변수를 특정 유형으로 좁히기 때문에 가능한 것이다.

never vs unknown?

  • never 타입은 절대 반환되지 않는 함수를 말하며, 실행이 종료되지 않는 무한 루프 함수나 오류를 발생하기 위해서만 존재하는 함수에서 사용된다.

  • unknown 타입은 함수의 실행은 완료되지만 값을 반환하지 않는 경우에 사용한다. 특히 런타임에 함수 본문에 return 문이 없다면 undefined를 반환하게 되는데 이 경우 void 타입을 사용하면 이런 실수를 방지할 수 있게 해 준다.

대체 다들 any는 언제 사용하라는 거지?

아래 예제와 같이 타입이 좁혀지지 못하는 경우에 사용하면 좋다. 만약 타입이 좁혀 질 수 있다면 안전하게 타입을 좁히는 방식으로 프로그래밍하는 것을 추천한다. https://ajdkfl6445.gitbook.io/study/typescript/condition-type-+-infer#condition-type

Last updated