[이펙티브 타입스크립트] 5장 any 다루기
아이템 42 모르는 타입의 값에는 any 대신 unknown을 사용하기
// ❌ 반환 타입으로 any 사용
function parseYAML(yaml: string): any {
// ...
}
// ❌ 이 경우에는 제네릭을 사용하는 건 권장하지 않는다.
function safeParseYAML<T>(yaml: string): T {
return parseYAML(yaml);
}
// 🟢 반환 타입으로 unknown 사용
function safeParseYAML(yaml: string): unknown {
return parseYAML(yaml);
}
일반적으로 타입스크립트 타입시스템은 한 집합은 다른 모든 집합의 부분 집합이면서 동시에 상위집합이 될 수 없다.
그러나 any는 어떠한 타입이든 any 타입에 할당 가능하고, any 타입은 어떠한 타입으로도 할당 가능하다.
반면에 unknown은 any와 동일하게 어떠한 타입이든 unknown 타입에 할당 가능하고, unknown은 unknown과 any에만 할당 가능하다.
let a: unknown = 'a';
let b: string = 'b';
// 🟢 unknown에는 어떤 타입도 할당 가능
a = b;
// ❌ Type 'unknown' is not assignable to type 'string'.
b = a;
함수의 반환값이나 변수를 unknown 타입으로 선언하면 적절한 타입으로 변환하도록 강제할 수 있다.
타입 단언이나 instanceof, 사용자 정의 타입 가드를 통해 타입을 안전하게 사용할 수 있다.
// 타입 단언
const book = safeParseYAML(`
name: Villette
author: Charlotte Bront
`) as Book;
interface Feature {
id?: string | number;
geometry: Geometry;
properties: unknown;
}
// 타입 가드
function processValue(val: unknown) {
if (val instanceof Date) {
// ...
}
}
function isBook(val: unknown): val is Book {
return (
typeof(val) === 'object' && val !== null &&
'name' in val && 'author' in val
);
}
function processValue(val: unknown) {
if (isBook(val)) {
// ...
}
}
이중 단언문에서도 any 대신 unknown이 권장된다.
declare const foo: Foo;
// ❌ 두 개의 단언문을 분리하면 any로 단언하게 된다.
let barAny = foo as any as Bar;
// 🟢 두 개의 단언문으로 분리해도 unknown 때문에 안전하게 사용할 수 있다.
let barUnk = foo as unknown as Bar;
{}은 null과 undefined를 제외한 모든 값을 포함하고, object는 null과 undefined를 제외한 모든 비기본형 타입이다.
null과 undefined가 불가능한 경우에는 unknown 대신 {}을 사용할 수 있다.
const a: {} = 'a';
const b: object = ['a'];
// ❌
const c: object = undefined;
const d: object = null;
const e: {} = undefined;
const f: {} = undefined;
아이템43 몽키 패치보다는 안전한 타입을 사용하기
자바스크립트는 객체와 클래스의 임의의 속성을 추가할 수 있다.
그렇지만 window와 DOM 노드에 임의의 속성을 추가하면 전역 변수가 되어 사이드 이펙트도 고려해야 한다는 문제가 있고, 타입스크립트에서는 타입 체커가 임의의 속성을 알지 못한다는 문제가 있다.
// ❌ Property 'monkey' does not exist on type 'Document'.
document.monkey = 'Tamarin';
임의의 속성을 추가하지 않고 데이터를 분리하는 것이 가장 좋지만, 불가능한 상황이라면 두 가지의 차선책이 존재한다.
1. interface의 특수 기능인 보강(augmentation)을 사용한다.
interface A {
name: string;
}
interface A {
age: number;
}
// 🟢
const b: A = {
name: 'John',
age: 10,
};
보강을 사용하면 타입 시스템을 사용할 수 있고, 속성에 주석을 붙일 수 있고, 자동완성을 사용할 수 있고, 몽키 패치가 어떤 부분에 적용되었는지 정확한 기록이 남는다.
그렇지만 모듈의 관점에서 import / export를 사용하는 경우에는 global 선언이 필요하다.
이렇게 하면 보강이 전역적으로 적용되기 때문에, 코드를 분리하기 어렵다.
// Document에 새로운 속성을 추가할 수 있다.
declare global {
interface Document {
monkey: string;
}
}
// 🟢
document.monkey = 'Tamarin';
2. 타입을 확장해서 사용한다 -> 더 권장
interface MonkeyDocument extends Document {
/** 몽키 패치의 속(genus) 또는 종(species) */
monkey: string;
}
// 더 구체적인 타입 단언을 사용한다.
(document as MonkeyDocument).monkey = 'Macaque';
타입을 확장하고 더 구체적인 타입 단언을 사용하는 방법은 Document 타입을 건드리지 않기 때문에 모듈 영역과 관련된 문제가 없다.
import한 곳에서만 확장한 타입을 사용하면 된다.
그렇지만 몽키 패치를 남용하는 것은 권장하지 않는다.
아이템 44 타입 커버리지를 추적하여 타입 안정성 유지하기
NoImplicitAny를 설정해도 명시적으로 any 타입을 선언하거나 서드파트 타입 선언에 포함된 경우에는 any가 여전히 존재할 수 있다.
type-coverage를 사용하면 any를 추적할 수 있다.
https://www.npmjs.com/package/type-coverage
서드파티의 경우 타입에 에러가 있거나, 공식 타입 선언이 없어 전체 모듈에 any 타입을 선언할 수 있다.
이런 경우 공식 타입 선언이 릴리즈되거나 커뮤니티에 공개된 타입 선언이 있을 수 있고, 타입 에러가 수정될 수도 있다.
이렇게 변경 가능성이 있기 때문에 주기적으로 타입 커버리지를 점검해서 불필요한 any를 제거하는 것이 좋다.