본문 바로가기

Develop/Frontend 가이드

[FE] Typescript - Type Guard 타입 가드

반응형

Type guard 타입 가드
Type guard 타입 가드

TypeScript - Type Guard 타입 가드

TypeScript 에서의 타입은 다른 언어와 의미가 다릅니다. 다른 언어에서 타입은 객체가 반드시 지켜야 하는 계약이라면, TypeScript 에서 타입은 객체에 대한 약속입니다. 다른 언어는 타입에 민감해서 타입이 맞지 않으면 사용할 수 없습니다. 하지만 TypeScript 에서 타입은 약속이고, 약속을 지키지 않는 객체에 접근해도 에러가 발생하지 않을 수 있습니다. 다만 컴파일할 때 개발자에게 문제가 있을 수 있다고 알려줍니다. 이처럼 TypeScript 의 타입은 유연하게 사용할 수 있는 특징이 있습니다. TypeScript 는 타입을 조합하여 사용할 수 있습니다. 그래서 아래와 같이 매개변수의 타입이 number 일수도 있고, string 일수도 있습니다.

function sample(data: number | string) : void {
  // data 로 number 타입이 넘어올 수도 있고 string 타입일 수도 있다.
}

TypeScript 의 특징 때문에 아래와 같이 코드를 작성하면 함수의 결과를 예측할 수가 없습니다.

function sample(data: number | string) : void {
  console.log(data + 1);
  // Error: Operator '+' cannot be applied to types 'string | number' and 'number'.
}

sample(1); // 2
sample('A'); // "A1"

함수의 매개변수로 여러 타입이 가능하기 때문에, 매개변수의 타입을 알아야 매개변수를 안전하게 사용할 수 있고 함수 실행 결과도 예측할 수 있습니다.

Type Guard 타입 가드 는 컴파일러가 타입을 예측할 수 있도록 코드를 작성해서 버그가 발생하지 않도록 예방하는 방법입니다. 정확하게 표현하면 TypeScript 컴파일러가 Type Inference 할 수 있도록 하는 방법입니다. TypeScript 컴파일러가 타입을 추론할 수 있도록 돕는 세부적인 방법을 하나씩 소개해드리겠습니다.

typeof type guards

TypeScript 컴파일러는 JavaScript 의 기능인 typeof 연산자를 이해하고 타입을 추론할 수 있습니다. 다만 JavaScript 와의 호환을 위해 typeof 로 타입 가드할 수 있는 타입은 JavaScript 에서 제공하는 타입만 가능합니다.

typeof 타입 가드는 'string' / 'number' / 'bigint' / 'boolean' / 'symbol' / 'undefined' / 'object' / 'function' 타입만 사용할 수 있습니다.

function sample(data: number | string) : void {
  // JavaScript 에서 사용하는 typeof 키워드의 의미를 TypeScript 컴파일러는 이해할 수 있다.
  if (typeof data === 'string') {
    // 위 if 조건문에 의해, if 문 내에서는 data 가 string 타입임을 컴파일러가 확신할 수 있다.
    console.log(data);
  } else {
    // TypeScript 컴파일러는 똑똑해서 data 가 'string' 타입이 아니라면 'number' 타입 뿐이라는 걸 이해할 수 있다.
    console.log(data + 1);
  }
}

Truthiness narrowing

JavaScript 는 if 조건문의 결과가 boolean 타입이 아니라도, 강제로 boolean 타입으로 변환하여 평가하는 특징이 있습니다. 예를 들어, 숫자 0, NaN, null, undefined 이 if 조건문에 주어지면 false 로 판단합니다. Truthiness narrowing 를 활용해서 null 이나 undefined 를 회피할 수 있습니다.

function sample(data: number[] | null) {
  // data 가 null 인지 검사한다. 
  // JavaScript 에서 `typeof null == 'object'` 의 결과가 true 이기 때문에, null 여부를 판별할 수 없다.
  // 따라서 if 조건문에 truthiness narrowing 을 사용해 null 여부를 판단한다.
  if (data && typeof data === 'object') {
    for (const number of data) {
      console.log(number);
    }
  }
}

Truthiness narrowing 을 사용하여 타입 가드를 할 때 아래와 같이 전체 코드를 감싸지 않도록 조심해야 합니다. 개발자가 truthiness narrowing 의 결과를 직관적으로 예측하기 힘든 부분이 있어, 예기치 못한 버그를 맞닥뜨릴 수도 있습니다. 따라서 truthiness narrowing 에만 의존하여 코드를 작성하는 건 좋은 코딩 습관이 아닙니다.

function sample(data: string | null) {
  // truthiness narrowing 로 코드 전체를 감싸는건 하지 말아야 한다.
  // data 이 빈 문자열일 경우 truthiness narrowing 는 false 로 판단하게 되며,
  // null 여부만 체크하려다가 빈 문자열 처리를 놓치게 될 수 있기 때문이다.
  if (str)
  {
    // ...
  }
}

Equality narrowing

TypeScript 컴파일러는 아래와 같이 등호 연산자로도 타입을 추론할 수 있습니다.

function sample(x: string | number, y: string | boolean) {
  // 매개변수 x 와 y 가 동일하다면, x 와 y 모두 string 타입이라 추론한다.
  if (x === y) {
    // x 와 y 모두 string 타입으로 취급한다.
  }

Truthiness narrowing 대신 equality narrowing 로 null 여부를 체크하는게 더 안전합니다.

function sample(data: string | null) {
  // truthiness narrowing 보다 명확하게 null 여부를 판별할 수 있다.
  // 이때 null 여부만 체크하는게 아니라 undefined 여부도 체크한다.
  if (data !== null)
  {
    // ...
  }
}

아래 코드처럼 프로퍼티 일부로 타입을 추론할 수도 있습니다.

type A = {
  kind: 'a',
  count: number
}
type B = {
  kind: 'b',
  name: string
}

function sample(data: A | B) {
  // A 타입과 B 타입에 공통으로 정의된 kind 프로퍼티를 equality narrowing 로,
  // data 가 어떤 타입인지 추론할 수 있다.
  if (data.kind === 'a') {
      console.log(data.count);
  } else {
      console.log(data.name);
  }
}

in 연산자 narrowing

in 연산자는 객체가 특정 프로퍼티를 가지고 있는지 파악하는데 활용하는 연산자입니다. TypeScript 는 in 연산자를 통해 타입을 추론할 수 있습니다. in 연산자로 타입가드하는 방법은 타입이 일치하는지 판단하는 다른 방법과 달리, 객체에 특정 프로퍼티가 존재하는지 판단합니다. 그래서 더 섬세하게 타입가드가 가능합니다.

type A = { a: () => void };
type B = { b: () => void };

function sample(data: A | B) {
  // 'a' 이라는 이름의 프로퍼티가 정의된 타입은 A 타입 뿐이므로,
  // TypeScript 는 data 을 A 타입이라 추론할 수 있다.
  if ('a' in data) {
    return data.a();
  }

  // A 타입이 아니라면, B 타입만 가능하므로 B 타입이라 추론할 수 있다.
  return data.b();
}

instanceof narrowing

TypeScript 는 instanceof 로 아래처럼 타입을 추론할 수 있습니다.

function sample(data: A | B) {
  if (data instanceof A) {
    // data 를 클래스 A 타입으로 추론한다.
  }
}

User-defined type guards

TypeScript 가 타입을 판단하는 방법을 직접 정의하거나, 타입을 판단하는 로직을 재사용하고 싶을 때 User-defined type guards 를 사용하면 됩니다.

type A = { a: () => void };
type B = { b: () => void };

// obj 의 타입이 A 타입인지 확인하는 user-defined type gurads 함수다.
function isA(obj: any): obj is A {
  // obj 에 a 프로퍼티가 있고 a 가 함수라면 A 타입이라 판단한다.
  return obj.a !== undefined && typeof obj.a == 'function';
}

function sample(data: A | B) {
  // user-defined type gourads 로 타입을 판별한다.
  if(isA(data)) {
      // TypeScript 는 isA 함수 결과가 true 라면, data 의 타입이 A 타입이라 추론한다.
    data.a();
  } else {
    data.b();
  }
}

sample({ a: () => console.log("A")})

참고

TypeScript : Narrowing
TypeScript Deep Dive : Type Guard

반응형