엄격하게 타입스크립트를 이용하는 9가지 방법

Sming·2022년 10월 3일
111
post-thumbnail

자바스크립트의 유연성때문에 나오게 된 타입스크립트! 하지만 올바르게 이용하지 않으면 안쓴것과 다름이 없습니다.

타입스크립트를 대부분 입문하시는 분들도 "any를 지양하자" 정도는 알고 있습니다. any를 쓰지 않는것에서 좀 더 나아가 타입스크립트를 좀 더 엄격하게 이용하는 법을 알아봅시다! 😎

1. 일단 엄격한 세팅부터 진행하자

타입을 엄격하게 작성하기전에 타입스크립트 환경을 엄격한 환경으로 할 필요가 있다. 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 를 이용하면 모든 옵션을 키게 됩니다.

2. any를 이용할바에 unknown을 이용하자

타입을 짜다보면 실제로 모든 타입이 들어갈 수 있는 상황이 나오게 됩니다. 이럴때 any를 이용하고 싶은 욕심이 있을 수 있습니다. 하지만 이런 상황에서는 unknown 타입을 사용하는것을 추천합니다.

unknownany의 차이는 타입이 집합에 포함되냐로 구분할 수 있을것 같습니다.

unknown은 타입의 최상위 집합이지만 any는 타입의 집합에 포함되지 않습니다. 그래서 unknown을 사용하고 함수 내부에서 타입 가드를 이용하게 되면 타입가드가 되어서 특정 타입을 추론할 수 있지만 any를 이용할시 타입가드가 되지 않는 것을 확인할 수 있습니다.

3. generic에는 항상 extends를 이용하자

타입을 동적으로 받을 수 있는 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로 추론도 가능합니다.

4. 옵셔널을 최대한 피하자

물론 옵셔널이 이용되어야 하는곳에는 옵셔널을 이용하는것이 맞습니다. 하지만 암묵적으로 옵셔널이 들어가는 타입들이 있습니다.

예를 들어 PropsWithChildren 이라는 react 내장 타입을 보겠습니다. 이 타입은 현재 props에 children속성까지 추가하는 타입입니다.

하지만 내부 코드를 보면 이렇게 생겼습니다.

type PropsWithChildren<P> = P & {children?: React.ReactNode};

보시면 알겠지만 children에 optional이 들어가는 것을 볼 수 있습니다. 유연하게 이용하기 위해서 이렇게 구현한것같습니다.

하지만 이렇게 구현할 시 children이 반드시 들어가야되는 상황에서 PropsWithChildren 은 에러를 잡아낼 수 없습니다.

type PropsWithStrictChildren<P> = P & {children: React.ReactNode};

구현하는데 크게 어려운 타입도 아니니 PropsWithStrictChildren 라는 커스텀 타입을 만들어서 반드시 children이 들어가는 곳에는 이런식으로 이용해도 됩니다.

5. 타입 단언은 최대한 피하자

타입 선언이라는 것이 있고 타입 단언이라는 것도 있습니다. 타입선언은 말그대로 타입을 선언하는 것입니다.

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만 나오는 것을 볼 수 있습니다.

6. null과 undefined는 확실하게 체크하자

typescript에서 타입가드를 가장 많이 이용하는곳은 undefinednull 처리일 것입니다.

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은 값으로 비교해주세요!

7. index signature 보다는 Record나 Mapped Type을 이용하자

객체의 모든 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타입에서 객체 내부의 키가 자동완성이 되는것을 확인할 수 있습니다.

8. 더 좁은 타입을 이용하자 (template literal)

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}`;

9. 튜플과 재귀를 이용하여 엄격한 타입만들기

이부분은 타입의 집합부분에서 좀 더 엄격하게 타이핑하는것이 아니라 사용자가 커스텀 타입으로 좀 더 엄격한 타이핑을 하는 것이다.

예를 들어 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 타입에 할당할 수 없습니다.
profile
딩구르르

6개의 댓글

comment-user-thumbnail
2022년 10월 4일

유익하네요. 2탄도 기대할게요

답글 달기
comment-user-thumbnail
2022년 10월 5일

레전드

답글 달기
comment-user-thumbnail
2022년 10월 10일

Great article

답글 달기
comment-user-thumbnail
2022년 10월 11일

7번의 Record 타입이 오타인 것 같습니다!

type Bar = Record<'a' | 'b' | 'c' | 'd', string>이 맞는 타입이네요.

포스트 잘 읽었습니다. 이렇게 타입스크립트를 다뤄주셔서 정말 감사드립니다.

답글 달기
comment-user-thumbnail
2022년 10월 25일

잘 정리하셨네요! 참고하겠습니다!

답글 달기
comment-user-thumbnail
2023년 10월 26일

자바를 공부하고 나서 보니 더 이해가 잘되네요 ...

답글 달기