[TypeScript] as 없이 스마트하게 타입 좁히기

아서·2023년 2월 25일
1
post-thumbnail

오로지 정답이 아닌 저의 생각이 많이 담긴 점 참고 부탁드려요.

타입단언(as)를 지양해야하는 이유는 다들 알고계시리라 믿겠습니다.

런타임에서 앱이 죽지 않으려면 타입가드를 잘 사용해야한다고 인지하고 있던 와중에 as로 인해 앱이 죽은 경험을 바로 하게되자 부들부들했습니다.

곧바로 타입가드를 붙히기 시작했고 덩달아 최대한 타입단언as을 걷어보기로 하였습니다.

아래 예시는

서버에서 **음료수이름**을 받아서 **음료수 별 색깔**을 이용

하는 설명을 위한 샘플코드입니다.

실수하기 쉬운 예시를 보여드리겠습니다.

const DRINK_COLOR = {
  '콜라': '#fe001a',
  '환타': '#ffa500',
  '웰치스': '#3eb489',
};
type Drink = keyof typeof DRINK_COLOR;

const drinkName: string = API().data.name;

return (
  <Text 
	color={DRINK_COLOR[drinkName as Drink]} // 위험!
  >
	{drinkName}
  </Text>
);

타입에러를 벗어나고자 as Drink와 같은 실수를 범하곤 합니다.
그러면 발생되는 문제는, drinkNameDrink타입이 아니라면 앱은 죽게됩니다.(promise, undefined는 크게 다루지 않겠습니다.)

또 다른 실수는 이 문제를 발견하고 해결하는 방식을

<Text 
  color={DRINK_COLOR[(drinkName || '') as Drink]}
>
  {drinkName}
</Text>

이렇게 하거나 ?. 옵셔널을 사용하거나 삼항연산자를 사용하는 등등등... 당장에 해결책에 포커스를 두게되어 클린하지 못하고 유지보수에 좋지 않다고 생각합니다.

그래서 타입가드를 붙이게 되었습니다.

const DRINK_COLOR = {
  '콜라': '#fe001a',
  '환타': '#ffa500',
  '웰치스': '#3eb489',
};
type Drink = keyof typeof DRINK_COLOR;

const drinkName: string = API().data.name;

if (drinkName in DRINK_COLOR){
  return (
    <Text 
	  color={DRINK_COLOR[drinkName as drinkName]}
    >
	  {drinkName}
    </Text>
  );
}

이렇게 타입가드(in연산자)를 하면 더 안전하게 as를 사용할 수 있습니다...!

...

...

...

는 제가 원하던 바가 아니었습니다. 타입 가드를 했지만 drinkName 타입은 여전히 string이라서 as를 계속 사용해야하는게 마음에 들지 않았습니다.

그래서...! 이를 해결할 inTypeGuard라는 함수를 구현했습니다...!

const inTypeGuard = <
  T extends string,
  K extends Record<string, any>,
>(
  key: T,
  object: K,
): keyof K | null => { // (1)
  if (key in object) { // (2)
    return key;
  }

  return null;
};

JS관점으로 보면 (2)if (key in object)안전하게 타입가드를 할 수 있게되고 TS관점으로는 (1): keyof K | null로 object의 key값들을 리턴타입으로 받아 컴파일을 통과할 수 있고 자동완성도 잘 뜨게 됩니다...!

아까 코드를 다시 수정하면

짠...!

import { inTypeGuard } from '@utils/types'

const DRINK_COLOR = {
  '콜라': '#fe001a',
  '환타': '#ffa500',
  '웰치스': '#3eb489',
};
type Drink = keyof typeof DRINK_COLOR;

const nameData: string = API().data.name;
const drinkName: Drink | null = inTypeGuard(nameData, DRINK_COLOR);

if (drinkName !== null){
  return (
    <Text 
	  color={DRINK_COLOR[drinkName]}
    >
	  {drinkName}
    </Text>
  );
}        
        

(아래 사진은 타입 증명을 위해 TS playground에 띄어보았습니다)

이렇게 drinkName의 타입이 '콜라' | '환타' | '웰치스' | null이 되면서 null 방어한다면 런타임으로도 지킬 수 있고 컴파일 타입도 이쁘게 등장하는 행복한 개선이 되었습니다...!(짝짝짝)

...

...

...

21세기 IT로 불가능이 없는 이 세상에서 이대로 마무리하기에는 2% 아쉬운 코드를 만들었다는 생각이 들었습니다.

저의 아쉬움은 2가지였습니다.

  1. return을 받아서 타입을 덮는 구조라서 변수 선언을 '한번 더' 해야한다.
  2. 꼭 이렇게 까지 해야만 기존 데이터API().data.namestring에서 벗어낼 수 있는건가???

그래서 기초부터 테스트를 해보았습니다.

별거없고 그냥 하드코딩 해봤어요(TS playground)

?????? 왜 잘됨???????

  1. 더 1차원적인 코드가 더 깔끔할 수도 있겠다는 생각...
  2. drinkData의 타입이 string이 아니라 "변하네" ???

...

마음을 가다듬고 다시 in 써보기

...

...

...

...
웨않뒘??

모든 작업을 멈추고 다시 "생각"부터 시작했습니다..

A === 'B' 이러한 "방식""타입 좁히기"가 가능하구나...

A === 'B'를 어떻게 타입으로 만들지?? 이걸 활용해서 함수를 만들고 싶은데 해야하지?? 이건 도대체 무슨 타입이지?

type Func = (value: string) => value === '콜라'; // error

??? 이게 뭐지?

(과거에 비슷한 경험을 떠올려 봅니다...)

아...!

type isCola = (value: string) => value is '콜라';

답은 "is"였다 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

(온갖 머릿 속 TS 지식을 이용하여 리펙토링중...)

(미리 test code 짜놔서 수월했던 건 안비밀)

...

진짜_최종결과물.ts !

export const inTypeGuard = <T extends Record<string, any>>(
  key: string,
  object: T,
): key is typeof object extends Record<infer R, any> ? R : never => {
  if (key in object) {
    return true;
  }

  return false;
};

제너릭과 infer를 활용!

이제 이쁘게 변화된 원래 음료수 로직을 한번 볼까요?

import { inTypeGuard } from '@utils/types'

const DRINK_COLOR = {
  '콜라': '#fe001a',
  '환타': '#ffa500',
  '웰치스': '#3eb489',
};
type Drink = keyof typeof DRINK_COLOR;

const drinkData: string = API().data.name;

if (inTypeGuard(drinkData)){
  return (
    <Text 
	  color={DRINK_COLOR[drinkData]} // Drink
    >
	  {drinkData}
    </Text>
  );
}        

밑에는 타입 잘 뜨는지 증명을 위한 TS Playground

와ㅏㅏㅏㅏ잘뜬다ㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏ!!!!!!!!!!!!!!!!!

infer 공부해놓기를 잘했다!!!

이렇게 feature 일들(시간과 작업물을 바꾸는) 주말의 나에게 맡기게 되었고 삶의질+=1, TS공부+=1, 팀생산성+=1, velog+=1을 얻었습니다

여러분도 이렇게 안전하고 이쁘게 타입스크립트 쓰세요~~

as 멈춰!

많은 피드백 모두 환영합니다~~~

profile
공리주의 개발자

0개의 댓글