아이템 21 타입 넓히기
런타임에 모든 변수는 유일한 값
을 가지고, 타입스크립트가 코드를 체크하는 정적 분석 시점에 변수는 가능한 값들의 집합인 타입
을 가진다. 그래서 상수를 사용해서 변수를 초기화할 때 타입을 명시하지 않으면 타입 체커가 타입을 결정해야 한다. 즉 지정된 단일 값을 가지고 할당 가능한 값들의 집합을 유추해야 한다.는 뜻이다. 그리고 이를 넓히기(widening)
라고 부른다.
1 | const mixed = ['x', 1]; |
위 예제에서 mixed는 추론 가능한 타입이 여러개이다. 만약 mixed에 대한 타입이 충분하지 않다면 어떤 타입으로 추론되어야 하는지 할 수 없어서 타입스크립트는 작성자의 의도를 추측한다.
넓히기 과정을 제어하는 방법
- const의 사용 : let 대신 const로 변수를 선언한다. const는 재할당이 불가하기 때문에 타입스크립트가 추론 가능한 타입이 여러개여서 모호한 과정에 빠지지 않는다. 다만 객체와 배열의 경우 const를 써도 문제가 있다.
1 | const v = { |
타입스크립트는 명확성과 유연성 사이의 균형을 유지하려고 한다. 그래서 구체적으로 타입을 추론해야 하지만, 잘못된 추론을 할 정도로 구체적으로 수행하진 않는다. 만약 타입 추론의 강도를 직접 제어하려면 타입스크립트의 기본 동작을 재정의한다.
타입스크립트의 기본 동작 제어방법
- 명시적 타입 구문 제공
- 타입 체커에 추가적인 문맥 제공
- const 단언만 사용
- 이 const는 변수 선언에 쓰이는 const와 가른 것이므로 헷갈려서는 안된다. const 단언문은 온전히 타입 공간의 기법이다.
- 아래 예제에서 값 뒤에
as const
를 작성하면 최대한 좁은 타입으로 추론하기 때문에 v3에서 넓히기가 동작하지 않는다. - 배열을 튜플로 추론할 때도
as const
를 쓸 수 있다.
1 | interface Vector3 { |
아이템 22 타입 좁히기
타입 넓히기의 반대개념이다.
타입스크립트가 넓은 타입으로부터 좁은 타입으로 진행하는 과정을 말한다.
- null 체크
1 | //1️⃣ |
위 예시에서 1️⃣의 el이 null이면 첫번 째 블록의 const el = document.getElementById(‘foo’); // Type is HTMLElement | null 을 실행하지 않기 때문에 Type is HTMLElement | null에서 null을 제외하므로 더 좁은 타입이 된다. 2️⃣와 같이 분기문에서 예외를 던지거나 함수를 반환해서 블록의 나머지 부분에서 변수의 타입을 좁힐 수도 있다.
- instanceof사용
- 속성 체크
- Array.isArray 사용
- 조건문 사용
1 | const el = document.getElementById('foo') // type is HTMLElement | null |
조건문이 가장 타입을 좁히는 데 능숙한 방법이지만 위와 같은 예제처럼 실수를 일으키기 쉽다. 위 예제에서 typeof null은 object
이기 때문에 if문에서 null이 제외되지 않는다. 다음과 같은 기본형에서도 기본형 값이 잘못되어도 제대로 좁혀지지 않는다. 빈 문자열 ‘’와 0 모두 false가 되기 때문에 좁혀지지 않는다.
1 | function foo(x?: number | string | null) { |
- 명시적 태그를 붙이기 (태그된 유니온 또는 구별된 유니온
- 사용자 정의 타입 가드 사용: 타입스크립트가 타입을 식별하지 못할 때 커스텀 함수를 도입할 수 있다.
- 타입 가드 사용
1 | const jackson5 = ['Jackie', 'Tito', 'Jermaine', 'Marlon', 'Michael'] |
아이템 23 한꺼번에 객체 생성하기
타입스크립트의 타입은 변경되지 않기 때문에 객체를 생성할 때는 속성을 하나씩 추가하는 것 보다 여러 속성을 포함해서 한꺼번에 생성해야 타입 추론에 유리하다.
1 | //에러 발생 |
객체를 반드시 제각각 나눠서 만들어야 한다면 타입 단언문(as)
를 사용한다. 그러나 객체를 한꺼번에 만드는 것이 더욱 권장된다. 작은 객체들을 조합해서 큰 객체를 만들어야 할 때는 전개 연산자
를 사용해서 객체를 한꺼번에 만든다. 이렇게 하면 타입 걱정 없이 필드 단위로 객체를 생성할 수 있는데 이때 중요한 점은 모든 업데이트마다 새 변수를 사용하여 새로운 타입을 얻도록 하는 것이다.
1 | //전개 연산자 사용 |
타입에 안전한 방식으로 조건부 속성을 추가하려면 속성을 추가하지 않는 null 또는 {}로 객체 전개를 사용한다.
1 | //1️⃣ 조건부 속성의 추가 - {} |
2️⃣의 타입은 유니온
으로 추론된다.
1 | const pharaoh: |
이 경우 pharaoh.start로 접근하면 start와 end는 항상 함께 정의되기 때문에 속성이 없다는 에러가 표시된다. 따라서 유니온을 사용하는 것이 가능한 값의 집합을 더 정확히 표현하는 것이다. 다만 유니온보다 선택적 필드가 다루기에 더 쉬울 수 있다. 아래와 같이 헬퍼 함수를 사용해서 표현한다.
1 | declare let hasMiddle: boolean |
아이템 24 일관성 있는 별칭 사용하기
별칭의 값을 변경하면 원래 속성값에서도 변경된다.
1 | const borough = { name: 'Brooklyn', location: [40.688, -73.979] } |
이렇게 별칭을 사용할 때 남발해서 사용하면 제어 흐름을 분석하기 어렵다. 제어 흐름을 분석하기 어려워서 오류가 발생할 수 있는데, 별칭은 일관성있게 사용한다
는 기본 원칙을 지키면 오류를 방지할 수 있다.
1 | interface Coordinate { |
위와 같이 별칭 대신 객체 비구조화를 사용해서 일관된 이름을 사용할 수 있는데, 배열과 중첩된 구조에서도 사용 가능하다. 다만 아래의 두 가지 문제를 주의한다.
- 전체 bbox 속성이 아니라 x와 y가 선택적 속성일 경우 속성 체크가 더 필요하다. 따라서 타입의 경계에 null 값을 추가하는 것이 좋다.
- bbox에는 선택적 속성이 적합했지만 holes에는 그렇지 않다. holes가 선택적이라면 값이 없거나 빈 배열이었을 것이다. 차이가 없는데 이름을 구별한 것이다. 빈 배열은 ‘holes없음’을 나타내는 좋은 방법이다.
별칭은 타입 체커 뿐 아니라 런타임에도 혼동을 야기할 수 있다.
1 | interface Coordinate { |
2️⃣의 경우 fn(polygon)을 호출하면 polygon.bbox를 제거할 가능성이 있다. 따라서 타입은 BoundingBox | undefined로 되돌리는 것이 안전하다. 그러나 함수를 호출할 때마나 속성 체크를 반복해야한다는 문제가 있다. 그래서 타입스크립트는 함수가 타입 정제를 무효화하지 않는다고 가정하지만 실제로는 무효화될 가능성이 있다.
TL;DR
- 별칭은 타입스크립트가 타입을 좁히는 것을 방해한다. 따라서 변수에 별칭을 사용할 때 일관되게 사용해야 한다.
- 비구조화 문법을 사용해서 일관된 이름을 사용하는 것이 좋다.
- 함수 호출이 객체 속성의 타입 정제를 무효화하 수 있다는 점에 주의한다. 속성보다 지역 변수를 사용하면 타입 정제를 믿을 수 있다.
아이템 25 비동기 코드에는 콜백 대신 async 함수 사용하기
자바스크립트는 비동기 동작을 모델링하기 위해 콜백 패턴
을 사용했는데, 필연적으로 콜백 지옥을 마주하여 직관적으로 코드를 이해하기 어려운 상황에 부딪히게 된다. 이를 극복하기 위해 프로미스, async await 키워드가 도입되어서 콜백 지옥을 간단하게 해결할 수 있게 되었다. ES5 또는 그 이전 버전을 대상으로 할 때 타입스크립트 컴파일러는 async와 await가 동작하도록 정교한 변환을 수행한다. 즉 런타임에 관계없이 async/await를 사용할 수 있다. async 함수는 항상 프로미스를 반환하도록 강제된다.
1 | const _cache: { [url: string]: string } = {} |
위 코드에서 async/await를 사용했기 때문에 requestStatus가 ‘success’로 끝나는 것이 명백해졌다. 콜백이나 프로미스를 사용하면 의도치한게 동기코드를 작성하게 되는 것처럼 실수로 반(half)동기 코드를 작성할 수 있지만 async를 사용하면 항상 비동기 코드를 작성하게 된다. 또한 async함수에서 프로미스를 반환하면 또 다른 프로미스로 래핑되지 않기 때문에 Promise<Promise<T>>
가 아닌 Promise<T>
가 된다.
TL;DR
- 콜백보다는 프로미스를 사용하는 게 코드 작성과 타입 추론 면에서 유리하다.
- 가능하면 프로미스를 생성하기보다 async와 await를 사용하는 것이 좋다. 간결하고 직관적인 코드를 작성할 수 있고 모든 종류의 오류를 제거할 수 있기 때문이다.
- 어떤 함수가 프로미스를 반환한다면 async로 선언하는 것이 좋다.
아이템 26 타입 추론에 문맥이 어떻게 사용되는지 이해하기
타입스크립트는 타입 추론 시 값만 고려하는 것이 아니라 그 값이 존재하는 곳의 문맥도 살핀다. 그렇기 때문에 가끔 이상한 결과가 나오기도 해서 타입 추론에 문맥이 어떻게 사용되는지 이해하는 것이 중요하다.
자바스크립트는 코드의 동작과 실행 순서를 바꾸지 않으면서 표현식을 상수로 분리해 낼 수 있다.
1 | function setLanguage(language: string) { |
인라인 형태의 타입스크립트는 함수 선언을 통해 매개변수가 language 타입이어야 한다는 것을 알고 있다. 타입스크립트는 일반적으로 값이 처음 등장할 때 타입을 결정하기 때문이다. 그러나 이 값으르 변수로 분리해내면 타입스크립트는 할당 시점에 타입을 추론한다. 그래서 아래와 같이 string으로 추론하고, Language 타입에 할당이 불가능해서 오류가 발생한다.
1 | type Language = 'JavaScript' | 'TypeScript' | 'Python' |
해결 방법
- 타입 선언에서 language의 가능한 값을 제한한다.
- language를 상수로 만든다.
1 | //타입 선언에서 language의 가능한 값을 제한한다. |
튜플 사용 시 주의점
튜플은 요소의 타입과 개수가 고정된 배열을 표현할 수 있는 타입이다. 따라서 아래와 같이 사용하면 에러가 발생한다.
1 | type Language = 'JavaScript' | 'TypeScript' | 'Python' |
loc로 선언하여서 타입이 number[]로 추론된다. 이는 길이를 알 수 없는 숫자의 배열이어서 [10,20]
과 맞지 않는 수의 요소이기 때문에 튜플에 할당할 수 없다. 이 에러를 해결하는 방법은 다음과 같다.
- 타입스크립트가 의도를 정확히 파악할 수 있도록 타입 선언 제공
- 상수 문맥 제공: as const로 값이 가리키는 참조와 그 값이 내부까지 상수임을 알려준다. 단 as const는 타입 정의에 실수가 있을 때 타입 정의가 아니라 호출되는 곳에서 에러가 발생해서 근본적인 원인을 찾기 어렵게 한다.
- readonly의 사용
1 | type Language = 'JavaScript' | 'TypeScript' | 'Python' |
객체 사용 시 주의점
객체 사용 시에도 에러를 발생시키는 문제가 생기는데 아래와 같은 방법으로 해결한다.
- 타입 선언 추가
- 상수 단언(as const)사용
콜백 사용 시 주의점
콜백을 다른 함수로 전달할 때 콜백의 매개변수 타입을 추론하기 위해 문맥을 사용한다. 그런데 아래 예시에서 fn으로 콜백을 상수로 뽑아내면 문맥이 소실되어 noImplicitAny오류가 발생한다.
1 | function callWithRandomNumbers(fn: (n1: number, n2: number) => void) { |
해결방법은 아래와 같다.
- 매개변수에 타입 구문을 추가한다.
- 가능하다면 전체 함수 표현식에 타입 선언
아이템 27 함수형 기법과 라이브러리로 타입 흐름 유지하기
로대시(lodash)와 같은 라이브러리의 일부 기능은 순수 자바스크립트로 구현되어 있고, 루프를 대체할 수 있기 때문에 유용하게 사용되고, 타입스크립트와 조합했을 때 더욱 유용하게 사용된다. 타입 정보는 유지하면서 타입 흐름이 계속 전달되도록 하기 때문이다. 하지만 서드파티 라이브러리 기반으로 코드를 짧게 줄이는데 시간이 많이 든다면 사용하지 않는게 낫다.
1 | const rows = rawRows |
References
[이펙티브 타입스크립트] 댄 밴더캄 지음