오로지 정답이 아닌 저의 생각이 많이 담긴 점 참고 부탁드려요.
타입단언(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
와 같은 실수를 범하곤 합니다.
그러면 발생되는 문제는, drinkName
이 Drink
타입이 아니라면 앱은 죽게됩니다.(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가지였습니다.
API().data.name
를 string에서 벗어낼 수 있는건가???그래서 기초부터 테스트를 해보았습니다.
별거없고 그냥 하드코딩 해봤어요(TS playground)
?????? 왜 잘됨???????
...
마음을 가다듬고 다시 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 멈춰!
많은 피드백 모두 환영합니다~~~