자바스크립트의 유연성때문에 나오게 된 타입스크립트! 하지만 올바르게 이용하지 않으면 안쓴것과 다름이 없습니다.
타입스크립트를 대부분 입문하시는 분들도 "any
를 지양하자" 정도는 알고 있습니다. any를 쓰지 않는것에서 좀 더 나아가 타입스크립트를 좀 더 엄격하게 이용하는 법을 알아봅시다! 😎
타입을 엄격하게 작성하기전에 타입스크립트 환경을 엄격한 환경으로 할 필요가 있다. tsconfig.json
에 다음과 같은 것을 설정할 시 좀더 엄격한 타입스크립트를 진행할 수 있다.
"noImplicitAny": true
-> 'any' 타입으로 구현된 표현식 혹은 정의 에러처리 여부"strictNullChecks": true
-> 엄격한 null 체크"strictFunctionTypes": true
-> 함수타입에 대한 엄격한 체크"strictBindCallApply": true
-> 엄격한 bind, call, apply 체크"strictPropertyInitialization": true
-> 클래스의 값 초기화에 대한 엄격한 체크"noImplicitThis": true
-> 'any' 타입으로 구현된 'this' 표현식 에러처리 여부"alwaysStrict": true
-> strict mode로 분석하고 모든 소스 파일에 "use strict"를 추가할 지 여부이렇게 많은 옵션이 있지만 사실 strict: true
를 이용하면 모든 옵션을 키게 됩니다.
타입을 짜다보면 실제로 모든 타입이 들어갈 수 있는 상황이 나오게 됩니다. 이럴때 any를 이용하고 싶은 욕심이 있을 수 있습니다. 하지만 이런 상황에서는 unknown
타입을 사용하는것을 추천합니다.
unknown
과 any
의 차이는 타입이 집합에 포함되냐로 구분할 수 있을것 같습니다.
unknown
은 타입의 최상위 집합이지만 any
는 타입의 집합에 포함되지 않습니다. 그래서 unknown
을 사용하고 함수 내부에서 타입 가드를 이용하게 되면 타입가드가 되어서 특정 타입을 추론할 수 있지만 any
를 이용할시 타입가드가 되지 않는 것을 확인할 수 있습니다.
타입을 동적으로 받을 수 있는 generic
은 일반적으로 이용하면 모든 타입을 받을 수 있습니다.
이렇게 이용하면 사실상 any
와 다를바가 없습니다. 모든 타입이 들어올 수 있기 때문이죠!
function foo<Type>(value: Type) {
// Type으로 모든 타입이 들어올 수 있다.
}
function foo<Type extends string | number> (value: Type) {
// Type으로 string 혹은 number만 가능하다.
}
foo(true) // error
밑에와 같이 이용하게 된다면 string, number 타입을 제외하고는 인자를 할당하는 곳에서 바로 에러를 던지게 됩니다. 그리고 string만 타입가드를 하게 되면 그 뒤는 알아서 number로 추론도 가능합니다.
물론 옵셔널이 이용되어야 하는곳에는 옵셔널을 이용하는것이 맞습니다. 하지만 암묵적으로 옵셔널이 들어가는 타입들이 있습니다.
예를 들어 PropsWithChildren
이라는 react 내장 타입을 보겠습니다. 이 타입은 현재 props에 children속성까지 추가하는 타입입니다.
하지만 내부 코드를 보면 이렇게 생겼습니다.
type PropsWithChildren<P> = P & {children?: React.ReactNode};
보시면 알겠지만 children에 optional이 들어가는 것을 볼 수 있습니다. 유연하게 이용하기 위해서 이렇게 구현한것같습니다.
하지만 이렇게 구현할 시 children이 반드시 들어가야되는 상황에서 PropsWithChildren
은 에러를 잡아낼 수 없습니다.
type PropsWithStrictChildren<P> = P & {children: React.ReactNode};
구현하는데 크게 어려운 타입도 아니니 PropsWithStrictChildren
라는 커스텀 타입을 만들어서 반드시 children이 들어가는 곳에는 이런식으로 이용해도 됩니다.
타입 선언
이라는 것이 있고 타입 단언
이라는 것도 있습니다. 타입선언은 말그대로 타입을 선언하는 것입니다.
interface Person {
name: string;
age: number;
}
타입 단언은 as
키워드를 이용하여 특정 타입을 as
뒤에 오는 타입으로 만들겠다는 뜻입니다.
const $Input = document.querySelector('#input') as HTMLInputElement;
원래 $Input
의 타입은 HTMLInputElement | null이 나오게 됩니다. 하지만 이것을 그냥 이용하게 되면 typescript는 요소가 Null일수도 있다라는 에러를 던집니다. 그래서 여기에 as
를 넣는 경우가 있는데 이것은 왜 좋지 않은 방식일까요??
타입 단언은 말그대로 개발자가 "너의 타입은 지금부터 이거야" 라고 정하는것입니다. document.querySelector
같은것은 무조건 null | Element
를 내보냅니다. 이는 실제로 돔을 찾지 못할때 null이 나오기 때문에 당연한 타입입니다. 하지만 여기서 개발자가 HTMLInputElement라고 단언을 해버리면 실제로 null이 나올수 있는 상황을 개발자가 무시해버리는 것이 되버립니다.
타입스크립트의 목적중 하나인 예기치 못한 undefined나 null문제를 잡을 수 있다
를 무시해버리는 것이죠.
타입 단언을 해결하는 방법은 간단합니다. 타입가드
를 이용하면 됩니다.
const $Input = document.querySelector('#input');
if($Input === null) {
throw new Error('요소를 찾지 못하였습니다.');
}
이렇게 이용할시 null
이 가드가 되어 HTMLInputElement
만 나오는 것을 볼 수 있습니다.
typescript에서 타입가드를 가장 많이 이용하는곳은 undefined
와 null
처리일 것입니다.
const someInput = number | undefined;
if(!someInput) {
throw new Error('에러');
}
위의 코드처럼 타입가드를 하게된다면 문제점은 무엇일까요?
바로 !someInput은 모든 falsy
값을 처리한다는 문제가 있습니다. falsy 값으로는 ''
0
NaN
null
undefined
false
같은 것이 있습니다.
가장 많은 실수로는 0이 있을 것 같습니다.
만약 someInput에서 index가 넘어온다고 가정하면 undefined
를 처리한다고 적어 놓은 타입가드가 0
일때도 동작하여서 예상치 못한 에러가 발생하게 됩니다.
const someInput = number | undefined;
if(typeof someInput === 'undefined') {
throw new Error('에러');
}
이렇게 명시적으로 undefined만 걸러주도록 타입가드를 작성하면 됩니다.
typeof null
을 이용할시 object가 나오니 null은 값으로 비교해주세요!
객체의 모든 key의 value가 하나의 타입이라면 굉장히 index signature
를 사용하고 싶다는 욕구가 들수도 있습니다.
const bar = {
a: '1';
b: '2';
c: 'gg';
d: 'bb';
}
type Bar = {
[key: string]: string
}
하지만 이것의 문제점은 뭘까요? 실제로 key를 string
으로만 정했기때문에 Bar의 타입으로 정의된 객체는 내부의 키가 자동완성
이 되지 않습니다.
const bar = {
a: '1';
b: '2';
c: 'gg';
d: 'bb';
}
// mapped type
type Bar = {
[key in 'a' | 'b' | 'c' | 'd']: string
}
// Record
type Bar = Record<'a' | 'b' | 'c' | 'd', string>
이렇게 키값을 정확하게 주게 된다면 Bar타입에서 객체 내부의 키가 자동완성
이 되는것을 확인할 수 있습니다.
string
타입이면 충분히 좁은 타입이라고 생각할 수 있지만 string보다 더 하위집합인 타입이 있다. 바로 template literal
타입이다.
예를 들어서 항상 YY-MM-DDTHH:MM:SS
형식으로 dateFormat이 들어온다고 생각하면 string
타입으로도 선언할수 있지만 template literal
을 이용하면 좀 더 자세하게 타이핑 할 수 있다.
type DateFormat = `${string}-${string}-${string}T${string}:${string}:${string}`;
이런 형식으로 작성할 수 있다.
또한 URL 형식도 좀 더 자세하게 작성할 수 있다.
type Url = `http${'s' | ''}://${string}`;
이부분은 타입의 집합부분에서 좀 더 엄격하게 타이핑하는것이 아니라 사용자가 커스텀 타입으로 좀 더 엄격한 타이핑을 하는 것이다.
예를 들어 100 ~ 200 사이의 number만 할당하고 싶은데 number타입을 쓰게되면 비교적 큰 타입처럼 느껴진다.
튜플과 타입의 재귀를 이용하면 숫자의 범위를 제한하는 타입을 만들 수 있습니다.
type CreateArray<
L extends number,
ARR extends unknown[] = []
> = ARR["length"] extends L ? ARR : CreateArray<L, [...ARR, 0]>;
type NumberRange<
ARR extends number[],
Max extends number,
RES extends number = never
> = ARR["length"] extends Max
? RES | Max
: NumberRange<[...ARR, 0], Max, RES | ARR["length"]>;
const num: NumberRange<CreateArray<2>,4> = 5 // error -> 5는 2 | 3 | 4 타입에 할당할 수 없습니다.
유익하네요. 2탄도 기대할게요