프론트엔드/TypeScript

[TypeScript] 타입 가드 (Type Guard)

타입 가드는 조건문 안에서 타입 범위를 한정시켜줄 수 있는 방법입니다.

이 글을 통해 타입 가드의 등장 배경, 개념, 사용 방법에 대해 알아보겠습니다.

 


등장 배경

interface Developer {
  name: string;
  skill: string;
}

interface Person {
  name: string;
  age: number;
}

const introduce = (): Developer | Person => {
  return { name: 'Kim', age: 20, skill: 'React' };
};

let kim = introduce();
console.log(kim.skill); // Error

유니온 타입은 공통된 속성(name 프로퍼티)에만 접근할 수 있습니다.

그래서 skill 속성에 접근하면 없는 프로퍼티로 간주가 됩니다.

 

💡 타입 단언 사용 시 문제점

if ((kim as Developer).skill) {
  let skill = (kim as Developer).skill;
  console.log(skill);
} else if ((kim as Person).age) {
  let age = (kim as Person).age;
  console.log(age);
}

타입 단언을 통해 skill 속성에 접근할 수 있도록 만들 수 있습니다.

하지만 조건문 내에서 타입 단언이 반복되며 비효율적인 코드가 됩니다.

 

이때 타입 가드를 통해 조건문 내에서 타입을 손쉽게 한정할 수 있습니다.

 


타입 가드 (Type Guard)

타입 가드는 조건문 안에서 타입 범위를 한정시켜줄 수 있는 방법입니다.

 


타입 가드 방법

1. typeof

원시 타입은 JavaScript의 typeof 연산자를 통해 간단하게 타입 가드를 할 수 있습니다.

하지만 커스텀 타입, 인터페이스 등의 복잡한 타입에는 typeof를 사용할 수 없습니다.

const func = (x: number | string) => {
  if (typeof x === 'string') {
    console.log(x.subtr(1)); // type x : string
    console.log(x.substr(1)); // type x : string
  }
  x.substr(1); // type x : number | string
};

 

 

2. instanceof

JavaScript의 instanceof 연산을 통해 객체가 특정한 클래스에 속하는지 확인할 수 있습니다.

class Foo {
  common = '123';
  foo = 123;
}

class Bar {
  common = '123';
  bar = 123;
}

const func = (arg: Foo | Bar) => {
  if (arg instanceof Foo) {
    console.log(arg.foo);
    console.log(arg.bar); // Error
  }

  if (arg instanceof Bar) {
    console.log(arg.foo); // Error
    console.log(arg.bar);
  }

  console.log(arg.common);
  console.log(arg.foo); // Error
  console.log(arg.bar); // Error
};

func(new Foo());
func(new Bar());

 

🔎 ESLint/max-classes-per-file

ESLint는 한 파일 안에 여러 클래스를 두는 것을 지양합니다.

이는 탐색이 어렵고 잘못된 구조의 코드를 만들 수 있기 때문입니다.

먼저 한 파일 당 하나의 클래스에 대해 단일 책임을 지도록 리팩토링을 해봅시다.

 

 

3. in

객체 내부에 특정 프로퍼티가 존재하는지 확인하는 연산자입니다.

interface Human {
  think: () => void;
}

interface Dog {
  tail: string;
  bark: () => void;
}

const func = (x: Human | Dog) => {
  if ('tail' in x) {
    x.bark(); // type x : Dog
  } else {
    x.think(); // type x : Human
  }
};

 

 

4. 리터럴 타입 가드

리터럴 타입은 ===, ==, !==, != 연산자 또는 switch를 통해 타입 가드를 할 수 있습니다.

특히 요소가 많아질수록 switch의 가독성과 성능이 더 좋습니다.

  • 리터럴 타입 : 특정 타입에 속하는 구체적인 값들 / ex) 문자열 리터럴 타입: 'cat', 'dog'..
type States = 'pending' | 'fulfilled' | 'rejected';


// 방법 1. 동등, 일치 연산자
const func = (state: States) => {
  if (state === 'pending') {
    pendingFunc(); // type state : 'pending'
  } else if (state === 'fulfilled') {
    fulfilledFunc(); // type state : 'fulfilled'
  } else {
    rejectedFunc(); // type state : 'rejected'
  }
};


// 방법 2. switch
const func = (state: States) => {
  switch (state) {
    case 'pending':
      pendingFunc(); // type state : 'pending'
      break;
    case 'fulfilled':
      fulfilledFunc(); // type state : 'fulfilled'
      break;
    case 'rejected':
      rejectedFunc(); // type state : 'rejected'
      break;
  }
};

 

 

5. 사용자 정의 타입 가드

커스텀 타입, 인터페이스 등의 복잡한 타입은 typeof, instanceof 등을 활용하기 어렵습니다.

또한 타입 판단 로직을 재사용하고 싶을 때가 생길 수 있습니다.

이때 사용자 정의 타입 가드를 사용하면 좋습니다.

const isDeveloper = (target: Developer | Person): target is Developer => {
  return (target as Developer).skill !== undefined;
};


if (isDeveloper(kim)) {
  kim.skill; // type kim : Developer
} else {
  kim.age; // type kim : Person
}

사용자 정의 타입 가드는 주로 isTypeName 형태의 함수명을 많이 사용합니다.

parameterName is Type의 의미는 매개변수가 해당 타입인지 구분하는 키워드로 해석됩니다.

리턴 값은 target 객체에 skill 메서드가 있다면 Developer 타입이 맞다는 의미의 불리언 값입니다. 

즉 isDeveloper 함수를 통과하고 나면 전달받은 인자가 Developer인지 아닌지를 리턴해줍니다.