tech.junha.kr

[Typescript] 타입 가드 (intersection, union, Type Guard) 가드 올려!

타입 가드(Type Guard)

배울 개념

  • intersection Type
  • union Type
  • Type Guard

1. 문제 발생

interface Animal {
  eat: () => void;
  sleep: () => void;
}
class Dog implements Animal {
  eat() {}
  sleep() {}
}
class Cat implements Animal {
  eat() {}
  sleep() {}
}

Dog클래스에 bark()메서드를 추가하고 싶은데?

  • 그러기 위해선, Animal인터페이스에 bark()를 추가해야하고, Animal인터페이스를 구현하는 Cat클래스도 bark()메서드를 구현해야하는 비효율이 발생한다.
  • 해당 타입을 쓰는 모든 코드에 변경을 가하지 않고, 특정 코드만 자유롭게 타입을 확장하는 방법 없을까?

2. Intersection Type

  • A & B: A 타입이면서 B 타입
type Human = {
  think: () => void,
};

type Developer = {
  work: () => void,
};

const junha: Human & Developer = {
  think() {
    console.log(`I'm thinking`);
  },
  work() {
    console.log(`I'm working`);
  },
};

언제 사용해?

  • Intersection Type은 기존 타입을 대체하지 않으면서, 새로운 필드를 추가하고 싶을 때 사용
type Human = {
  think: () => void;
};
type Developer = {
  work: () => void;
};
type Rich = {
  flex: () => void;
};

const junha: Human & Developer & Rich = {
  think() {
    console.log(`I'm thinking`);
  },
  work() {
    console.log(`I'm working`);
  },
  flex() {
    console.log(`I'm flexing`);
  },
};
  • 기존의 Human & Developer 타입의 junha에 Rich 타입을 추가해 기존 타입은 변경하지 않고 확장할 수 있다.

주의!! 각 타입에 겹치는 필드가 있다면 어떻게 될까?

  • intersection 하려는 두 타입의 필드가 다르다면 오류를 출력한다.
//  develop 메서드 선언, 매개변수는 없고, 반환 타입은 void이다
type Developer = {
  develop: () => void;
};

type Designer = {
  design: () => void;
};

const 개자이너: Developer & Designer = {
  develop() {
    console.log("I'm programming");
  },
  design() {
    console.log("I'm designing");
  },
};

개자이너.develop(); // I'm programming 출력
개자이너.design(); // I'm designing 출력

3. Union Type

  • A | B: A 타입, B 타입 둘 중 하나
let one: string | number;
one = '1';
one = 1;

언제 사용해?

  • 여러 타입 중 하나가 올 것이라고 가정할 때 사용한다.
  • 하지만 인터페이스는 유니온 타입을 확장하지 못한다.
  • 이때는 type 키워드와 &(Intersection Type)을 사용해 확장한다.
type A = string | number;

interface StrOrNum extends A {
  a: string;
}
// error An interface can only extend
// an object type or intersection of object types with statically known members.

type StrOrNum = {
  a: string;
} & (string | number);
// Ok

주의!! Union Type은 동시에 여러 타입이 될 수 없다.

type Human = {
  think: () => void;
};
type Dog = {
  bark: () => void;
};

declare function getType(): Human | Dog;

const junha = getType();
junha.think(); // Property 'think' does not exist on type 'Dog'.
junha.bark(); // Property 'bark' does not exist on type 'Human'.
  • junha의 타입은 유니언 타입으로 선언되었기 때문에, Human인지 Dog인지 확신할 수 없다.
  • 따라서 think, bark 둘 중 어느 메서드도 사용하지 못한다.
  • junha.think() 함수를 호출할 때 junha가 Human이라면 괜찮지만 Dog라면 think를 호출할 수 없기 때문에 컴파일 단계에서 에러가 나온다.
  • 이 에러는 타입 가드를 통해 해결할 수 있다.

4. Type Guard

1) 타입 가드란?

  • 데이터의 타입을 알 수 없거나, 될 수 있는 타입이 여러 개라고 가정할 때, 조건문을 통해 데이터의 타입을 좁혀나가는 것을 말한다.
  • 데이터의 타입에 따라 대응하여 에러를 최소화할 수 있다.
  • 타입을 통해 ‘가드’하는 코드, 방어적인 코드를 짤 수 있다.

2) 타입 가드를 사용해 유니언 타입을 제대로 사용해보자

type Human = {
  think: () => void;
};
type Dog = {
  tail: string;
  bark: () => void;
};

declare function getType(): Human | Dog;

const junha = getType();

// here is the clue
if ('tail' in junha) {
  junha.bark();
} else {
  junha.think();
}

3) What happend?!

  • 타입스크립트에게 타입을 추론할 수 있도록 단서를 주었다!
  • 이렇게 타입을 구별할 수 있는 단서가 있는 유니온 타입은 구별된 유니온라고(태그된 유니온, 서로소 유니온)한다.

4) 실무에서 자주 쓰는 구별된 유니온과 타입 가드

type SuccessResponse = {
  status: true;
  data: any;
};
type FailureResponse = {
  status: false;
  error: Error;
};

type CustomResponse = SuccessResponse | FailureResponse;

declare function getData(): CustomResponse;

const response: CustomResponse = getData();
if (response.status) {
  console.log(response.data);
} else if (response.status === false) {
  console.log(response.error);
}
  • 서버에서 오는 응답 또는 함수의 결과가 여러 갈래로 나뉠 때 구별된 유니온 사용 가능
  • 타입의 단서를 토대로 타입 가드를하고, 응답의 결과에 따라 다른 작업을 실행시켜준다.

5) 다양한 연산자를 통한 타입 가드

01. instanceof

  • 클래스도 타입이다. 객체가 어떤 클래스의 객체인지 구별할 때 사용하는 연산자
  • 인스턴스 instanceof 클래스와 같이 사용한다.
class Developer {
  develop() {
    console.log(`I'm working`);
  }
}
class Designer {
  design() {
    console.log(`I'm working`);
  }
}
const work = (worker: Developer | Designer) => {
  if (worker instanceof Designer) {
    worker.design();
  } else if (worker instanceof Developer) {
    worker.develop();
  }
};

02. typeof

  • 데이터의 타입을 반환하는 연산자
  • typeof 데이터 === 'string' 과 같이 사용
  • typeof 데이터 === 'undefined' 처럼 undefined 체크도 가능
  • 참고로 데이터 == 'null'과 같이 쓰면 null, undefined 둘 다 체크
const add = (arg?: number) => {
  if (typeof arg === 'undefined') {
    return 0;
  }
  return arg + arg;
};

03. in

  • key in obj: obj.key의 존재여부 리턴
type Human = {
  think: () => void;
};
type Dog = {
  tail: string;
  bark: () => void;
};

declare function getType(): Human | Dog;

const junha = getType();
if ('tail' in junha) {
  junha.bark();
} else {
  junha.think();
}

04. literal type guard

  • 리터럴 타입: 특정 타입의 하위 타입. 구체적인 타입. string 타입의 리터럴 타입은 ‘cat’, ‘dog’
  • 리터럴 타입은 동등(==), 일치(===) 연산자 또는 switch로 타입 가드 가능
  • 참고로 예제처럼 하나의 value 값에 따라 조건문을 작성하는 경우, 대부분의 경우 switch의 연산 결과가 if-else보다 더 빠르므로 되도록 switch를 쓰는 걸 권장해 드립니다. 조건문의 개수가 적으면 큰 차이는 없지만, 조건문의 개수가 많아질수록 switch의 성능이 더 빠르다.
type Action = 'click' | 'hover' | 'scroll';

const doPhotoAction = (action: Action) => {
  switch (action) {
    case 'click':
      showPhoto();
      break;
    case 'hover':
      zoomInPhoto();
      break;
    case 'scroll':
      loadPhotoMore();
      break;
  }
};

const doPhotoAction2 = (action: Action) => {
  if (action === 'click') {
    showPhoto();
  } else if (action === 'hover') {
    zoomInPhoto();
  } else if (action === 'scroll') {
    loadPhotoMore();
  }
};

05. 사용자 정의 함수

import is from '@sindresorhus/is';

const getString = (arg?: string) => {
  if (is.nullOrUndefined(arg)) {
    return '';
  }
  if (is.emptyStringOrWhitespace(arg)) {
    return '';
  }
  return arg;
};
  • type guard에 유용한 오픈소스 중 sindresorhus/is를 사용하여 가
    독성 있게 타입을 체크할 수 있다.
  • Yarn add @sindersorhus/is, npm install @sindersorhus/is