타입 추론에서 발생할 수 있는 몇가지 문제와 그 해법을 안내한다.
타입스크립트가 타입을 추론할 수 있다면 타입구문을 작성하지 않는게 좋다.
(예를 들면 const a:number = 10
와 같이 이미 10이란 숫자를 통해 타입을 알 수 있는데, 굳이 number
를 명시할 필요는 없다.)
어떤 언어들은 매개변수의 최종 사용처까지 참고하여 타입을 추론하지만, 타입스크립트는 최종 사용처까지 고려하지 않았다. 타입스크립트에서 변수의 타입은 일반적으로 처음 등장할 때 결정된다.
비구조화 할당문은 모든 지역 변수의 타입이 추론되도록 한다.
린터를 사용하고 있다면 eslint 규칙 중 no-inferrable-types
을 사용하여 작성된 모든 타입 구문이 정말로 필요한지 확인할 수 있다.
let
대신 const
로 변수를 선언하게 된다. const
로 변수를 선언하면 코드가 간결해지고 타입체커가 타입을 추론하기에도 좋다.function fetchProduct(id: string) {}
function fetchProductBySerialNumber(id: number) {}
const id = "12-34-56";
fetchProduct(id);
{
const id = 123456; // OK
fetchProductBySerialNumber(id); // OK
}
동일한 변수명에 타입이 다르다면 타입스크립트 코드는 잘 동작할지 몰라도 사람에게 혼란을 줄 수 있다. 변수의 값은 바뀔 수 있지만 타입은 일반적으로 바뀌지 않는다는 사실을 기억한다.
상수를 사용해서 변수를 초기화 할 때 타입을 명시하지 않으면 타입체커는 타입을 결정해야 한다. 이 말은 지정된 단일 값을 가지고 할당 가능한 값들의 집합을 유추해야 한다는 뜻이다. 이 과정을 타입스크립트에서는 넓히기(widening)라고 부른다.
타입 넓히기가 진행될 때, 주어진 값으로 추론 가능한 타입이 여러개가 되면서 과정이 상당이 모호하고, 명확성과 유연성 사이의 균형을 유지하려고 한다.
const
를 사용하기const
단언문(as const
)을 사용하기null
체크const el = document.getElementById('foo'); // Type is HTMLElement | null
if (!el) throw new Error('Unable to find #foo');
el; // Now type is HTMLElement
el.innerHTML = 'Party Time'.blink();
instanceof
를 사용하기function contains(text: string, search: string|RegExp) {
if (search instanceof RegExp) {
search // Type is RegExp
return !!search.exec(text);
}
search // Type is string
return text.includes(search);
}
interface A { a: number }
interface B { b: number }
function pickAB(ab: A | B) {
if ('a' in ab) {
ab // Type is A
} else {
ab // Type is B
}
ab // Type is A | B
}
function contains(text: string, terms: string|string[]) {
const termList = Array.isArray(terms) ? terms : [terms];
termList // Type is string[]
// ...
}
interface UploadEvent { type: 'upload'; filename: string; contents: string }
interface DownloadEvent { type: 'download'; filename: string; }
type AppEvent = UploadEvent | DownloadEvent;
function handleEvent(e: AppEvent) {
switch (e.type) {
case 'download':
e // Type is DownloadEvent
break;
case 'upload':
e; // Type is UploadEvent
break;
}
}
이 패턴은 태그된 유니온(tagged union) 또는 구별된 유니온(discriminated union)이라고 불리며, 타입스크립트 어디에서나 찾아볼 수 있다.
function isInputElement(el: HTMLElement): el is HTMLInputElement {
return 'value' in el;
}
function getElementContent(el: HTMLElement) {
if (isInputElement(el)) {
el; // Type is HTMLInputElement
return el.value;
}
el; // Type is HTMLElement
return el.textContent;
}
{...a, ...b}
)를 사용하면 된다.declare let hasMiddle: boolean;
const firstLast = {first: 'Harry', last: 'Truman'};
function addOptional<T extends object, U extends object>(
a: T, b: U | null
): T & Partial<U> {
return {...a, ...b};
}
const president = addOptional(firstLast, hasMiddle ? {middle: 'S'} : null);
president.middle // OK, type is string | undefined
async
함수 사용하기콜백보다는 프로미스를 사용하는게 코드 작성과 타입 추론면에서 유리하다.
async
함수에서 프로미스를 반환하면 또 다른 프로미스로 래핑되지 않는다. 반환타입은 Promise<Promise<T>>
가 아닌 Promise<T>
가 된다.
변수를 뽑아서 별도로 선언했을 때 오류가 발생한다면 타입 선언을 추가해야 한다.
변수가 정말로 상수라면 상수 단언(as const
)를 사용해야 한다. 그러나 상수 단언을 사용하면 정의한 곳이 아니라 사용한 곳에서 오류가 발생하므로 주의해야 한다.
내장된 함수형 기법들과 로대시 같은 라이브러리에 타입 정보가 잘 유지되는 이유는 함수 호출 시 전달된 매개변수 값을 건드리지 않고 매번 새로운 값을 반환함으로써 새로운 타입으로 안전하게 반환할 수 있다.
타입 흐름을 개선하고, 가독성을 높이고, 명시적인 타입 구문의 필요성을 줄이기 위해 직접 구현하기보다는 내장된 함수형 기범과 로대시 같은 유틸리티 라이브러리를 사용하는 것도 좋다.