개발/type-challenges

타입스크립트 타입 정복: Type Guards와 Narrowing

JonghwanWon 2022. 8. 2. 00:34

* 이 프로젝트는 기본적으로 TypeScript를 써 보았고, 내장된 유틸리티 타입들과 Generic 등을 알고 있는 상태에서 진행하는 것을 추천합니다. 그렇지 않다면, 공식 사이트 등을 참고해가며 학습과 병행하는 것을 추천합니다.


Narrowing과 type guards

Narrowing은 type guards를 사용해 type을 좁혀 더 정확하게 만드는 과정입니다.

type guards는 type의 범위를 좁히는 데 도움을 주는 특별한 검사 방식을 말합니다.

 

이해가 쉽도록 예제로 풀어보겠습니다.

 

여기, 천 단위 구분 기호를 추가해주는 함수가 있습니다.

export function withCommas(value: number) {
  const formatter = new Intl.NumberFormat('ko-KR');
  return formatter.format(value);
}

생성된 formatter의 format 메서드는 number와 bigint 타입을 받을 수 있도록 설계되어 있습니다.

Intl.NumberFormat.format method

만약, withCommas함수가 인수로 숫자로 변환이 가능한 string타입(e.g. '123')을 허용한다고 생각해봅시다.

TypeScript는 Argument of type 'string' is not assignable to parameter of type 'number | bigint'. 에러가 발생하게 됩니다.

 

전달받은 value를 parseInt 함수를 이용해 숫자로 변환해봅시다.

parseInt 함수는 string타입을 인수로 취하고 있는데 value는 string 또는 number 타입이기 때문에 에러가 발생합니다.

이를 회피하기 위해 Number로 감싸 형 변환시켜줄 수도 있지만 앞으로 알아볼 typeof 키워드로 타입을 좁혀보겠습니다.

 

string이라면 parseInt로 변환하고 그렇지 않으면 인수를 그대로 사용하도록 수정해봅시다.

export function withCommas(value: number | string) {
  const numericValue = typeof value === 'string' ? parseInt(value) : value;
  const formatter = new Intl.NumberFormat('ko-KR');

  if (Number.isNaN(numericValue)) {
    throw new Error(`${value} is not numeric.`);
  }

  return formatter.format(numericValue);
}

이제 withCommas 함수는 number뿐 아니라 string을 받아 처리가 가능해졌습니다. 🎉

typeof value === 'string'처럼 타입을 좁혀나가는 과정Narrowing이라 합니다.

 

조금 더 살펴볼까요?

type guards

공식 문서에 따르면 TypeScript는 타입을 좁히는 데 사용할 수 있는 다양한 type guards가 있습니다

매우 다양한 형태가 존재하는데 그중 사용 빈도가 가장 높은 세 가지 방법을 먼저 살펴보겠습니다.

typeof type guard

JavaScript는 런타임에서 값의 유형에 대해 아주 기본적인 정보를 알 수 있도록 typeof 연산자를 지원합니다.

TypeScript는 typeof 연산자가 특정 문자열을 반환할 것으로 예상합니다.

 

typeof 연산자의 반환 값

- "string"
- "number"
- "bigint"
- "boolean"
- "symbol"
- "undefined"
- "object"
- "function"

typeof null의 반환 값은 "object"인 것에 주의하세요. "null"을 반환하지 않습니다!
그럼 null 비교는 어떻게 하나요? === null으로 깊은 비교를 통해 가능합니다.

typeof 연산자는 여러 JavaScript 라이브러리에서 자주 볼 수 있으며, TypeScript에서는 이를 narrow types로 이해할 수 있습니다.

function someFunction(param: string | number) {
  if (typeof param === 'string') {
    console.log(param.length); // param은 이 블럭에서 string 입니다.
  } else {
    console.log(param.toFixed(1)); // param은 이 블럭에서 number 입니다.
  }
}

in operator narrowing

in 연산자를 통한 방법을 알아보겠습니다.

아래 예제에서는 이름을 가진 기본 선수 타입을 확장해 달리기 선수, 수영선수의 타입을 만들었습니다.

type Athlete = { name: string };

type Runner = Athlete & { run: () => void };
type Swimmer = Athlete & { swim: () => void };

그리고 선수를 움직이는 함수 moveAthlete를 만들어 봅시다.

이 함수는 Runner와 Swimmer 유니언 타입의 athlete를 인수로 받습니다.

function moveAthlete(athlete: Runner | Swimmer) {
  athlete.run();
}

 

예상하듯 TypeScript는 Swimmer 타입에는 run() 메서드가 없기 때문에 에러가 발생합니다.

앞에서 알아본 typeof를 통한 type guard는 Runner Swimmer에 대해 'object'를 반환하므로 정확히 추론해 낼 수 없습니다.

 

대신 in operator를 통해 추론해 낼 수 있습니다.

Javascript의 in 연산자는 지정한 속성이 개체에 있으면 true를 반환합니다.

따라서 athlete에 'run' 속성이 있는지 확인해보면 좋겠습니다.

 

function moveAthlete(athlete: Runner | Swimmer) {
  if ('run' in athlete) {
    athlete.run(); // athlete는 이 코드블럭에서 Runner 입니다.
  } else {
    athlete.swim(); // athlete는 이 코드블럭에서 Swimmer 입니다.
  }
}

 

Using type predicates

이번에는 TypeScript 컴파일러에게 특정한 값이 어떤 유형인지 알려주는 type predicates를 알아보겠습니다.

type predicates는 반환 타입에 어떤 유형인지 정의하는 함수입니다.

 

위의 예제와 동일한 타입(Runner, Swimmer)이 존재할 때 Runner인지 확인하는 함수를 만들어봅시다.

function isRunner(athlete: Runner | Swimmer): athlete is Runner {
  return (athlete as Runner).run !== undefined;
}

인수의 타입을 먼저 as Runner로 단언해 준 다음 run 메서드의 비교를 통해 boolean 값을 반환합니다.

만약 true라면 athlete는 Runner타입이 되고, false라면 Swimmer타입이 됩니다.

const athlete = getAthlete();

if (isRunner(athlete)) {
  athlete.run();
} else {
  athlete.swim();
}

type predicates를 통한 narrowing은 유용하고 많이 사용됩니다.

const athletes = [getAthlete(), getAthlete(), getAthlete()]; // (Runner|Swimmer)[]

const runners = athletes.filter(isRunner); // Runner[]

 

여기까지 type narrowing에 대해 간단하게 알아보았습니다.

여기서 설명하지 못한 기법들은 공식문서에서 찾아보시면 좋을 것 같습니다.

 

앞으로 여기서 다루지 않은 방법들은 type narrowing 2편에서 다루어보겠습니다.

const runners = athletes.filter(isRunner);
const runners2 = athletes.filter((athlete) => isRunner(athlete));

이후에는 부록으로 위 예제 코드의 차이와 나아가 Opaque에 대해서 다루어 볼 예정입니다.

 

마지막으로 type-challenges의 LookUp 문제 소개해드리며 글을 마무리합니다.

긴 글 읽어주셔서 감사합니다.

 

GitHub - type-challenges/type-challenges: Collection of TypeScript type challenges with online judge

Collection of TypeScript type challenges with online judge - GitHub - type-challenges/type-challenges: Collection of TypeScript type challenges with online judge

github.com