타입 이름을 짓는 것 또한 타입 설계에서 중요한 부분이다.
같은 의미에 다른 이름을 붙이기보다, 특별한 의미가 있을 때만 용어를 구분해야 한다.
// 이렇게 짓기 보다
interface Animal {
name: string;
endangered: boolean;
habitat: string;
}
// 이렇게 짓자
interface Animal {
commonName: string;
genus: string;
species: string;
status: ConservationStatus;
climates: KoppenClimate[];
}
type ConservationStatus = 'EX' | 'EW' | 'CR' | 'EN' | 'VU' | 'NT' | 'LC';
type KoppenClimate = 'Af' | 'Am' | 'As' | 'Aw' | 'BSh' | 'BSk' | 'BWh' | 'BWk'; // 더 있음..
자체적으로 용어를 만들어내기보다 해당 분야에 이미 존재하는 용어를 사용하자. 이렇게 하면 타입의 명확성을 올릴 수 있다. 좋은 이름은 추상화의 수준을 높이고 의도치 않은 충돌의 위험성을 줄여준다.
타입스크립트는 구조적 타이핑 때문에 가끔 이상한 결과를 발생시킬 수 있다. (책에서는 Vector2D
를 위한 함수에 Vector3D
가 들어왔을 때에도 문제 없음을 예시로 가져옴)
→ 이러한 경우 ‘상표(_brand)’를 이용한다. 이 기법은 타입 시스템에서만 동작하여 런타임 오버헤드를 줄이는 효과가 있다. (런타임에 상표를 검사하는 것과 동일한 효과를 얻을 수 있다.)
보통 함수를 만들어서 타입 단언문을 사용하여 그 값의 타입이 상표 타입인지를 확인하는 방법으로 쓴다.
// 이진 탐색시 목록이 정렬되어있는지 확인하는 상표 기법
type SortedList<T> = T[] & { _brand: 'sorted' };
const isSorted = <T>(xs: T[]): xs is SortedList<T> => {
for (let i = 1; i < xs.length; i++) {
if (xs[i] < xs[i - 1]) {
return false;
}
}
return true;
};
// 이것 보다
function f1() {
const x: any = expressionReturningFoo();
processBar(x);
}
// 이게 낫다.
function f2() {
const x = expressionReturningFoo();
processBar(x as any)
}
이게 더 좋은 이유는
x
라는 타입이processBar
의 호출 이후에도f1
에서는any
인 반면f2
에서는 호출할 때만any
타입이 되기 때문이다. 또한 반환타입이any
인 경우 타입 안정성이 나빠지므로 절대 하면 안된다.
function f1() {
const x = expressionReturningFoo();
// @ts-ignore
processBar(x);
return x;
}
any
대신 오류를 제거하기 위해@ts-ignore
를 사용할 수 있다.
// 이렇게 any를 사용하는 것보다
const config: Config = {
a: 1,
b: 2,
c: {
key: value
}
} as any;
// 이렇게 범위를 좁혀서 사용하는 것이 낫다.
const config: Config = {
a: 1,
b: 2,
c: {
key: value as any
}
}
최소한의 범위에만 any를 사용하자!
// 이렇게 하지 말자
const getLengthBad(array: any) {
return array.length
}
// 이렇게 하는것이 낫다.
const getLength(array: any[]) {
return array.length;
}
3가지의 이유로 위보다 아래의 것이 낫다.
- 함수 내의
array.length
타입 체크- 함수의 반환 타입이
any
대신number
로 추론- 함수 호출될 때 매개변수가 배열인지 체크
함수의 매개변수가 객체이지만 값을 알 수 없을때 any
보다 아래의 것들을 사용하는 것이 좋다.
{[key: string]: any}
object
unknown
타입또한 함수 자체도 any
타입을 갖는것 보다 반환 타입을 지정해주어서 사용하는 것이 더 구체적인 형태를 사용할 수 있는 것이다. ex) type Fn0 = () => any;
→ any
를 사용할 때 정말로 모든 값이 허용되어야만 하는지 면밀히 검토하고 사용하자.
내부 로직이 복잡하여 안전한 타입으로 구현하기 어려운 경우, 함수 내부에는 타입 단언을 사용하고 함수 외부로 드러나는 타입 정의를 정확히 명시하는 정도로 끝내는게 낫다.
안에서 예시로 cacheLast
라는 함수를 가져와서 다음과 같이 내부에 단언문을 사용하였지만 호출하는 쪽에서 그것을 알지 못하기 때문에 괜찮다고 설명한다.
function cacheLast<T extends Function>(fn: T): T {
let lastArgs: any[] | null = null;
let lastResult: any;
return function (...args: any[]) {
if (!lastArgs || !shallowEqual(lastArgs, args)) {
lastResult = fn(...args);
lastArgs = args;
}
return lastResult;
} as unknown as T;
}
마찬가지로 뒤에 나오는 예제에서도 실제 오류가 아니라는 것을 알고 있는 경우 any로 단언해서 함수 내부에서 정의하는 것을 확인할 수 있다.
→ 타입 선언문은 일반적으로 타입을 위험하게 만들지만 상황에 따라 현실적인 해결책이 되기도 한다. 불가피하게 사용하는 경우, 정확한 정의를 가지는 함수 안으로 숨기도록 한다.