42. 모르는 타입의 값에는 any 대신 unknown을 사용하기
1. 함수의 반환값과 관련된 unknown
함수의 반환 타입으로 any를 사용하는 것은 좋지 않은 설계다.
function parseYAML(yaml: string): any {}
interface Book {
name: string;
author: string;
}
const book = parseYAML(
`name: Jane Eyre
author: CharlotteBronte`
);
console.log(book.title); // 오류 없음. 런타임에 "undefined" 경고
book("read"); // 오류 없음. 런타임에 타입 에러 발생
대신 함수가 unknown 타입을 반환하게 만드는 것이 더 안전하다.
function safeParseYAML(yaml: string): unknown {
return parseYAML(yaml);
}
// ...
console.log(book.title); // Object is of type 'unknown'
book("read"); // Object is of type 'unknown'
unknown 타입을 이해하기 위해서는 할당 가능성의 관점에서 any를 생각해야 한다.
- 어떠한 타입이는 any 타입에 할당 가능하다.
- any 타입은 어떠한 타입으로도 할당 가능하다.
any는 다른 모든 집합의 부분 집합이면서 동시에 상위집합인 것이다.
타입 체커는 집합 기반이기 때문에 any를 사용하면 타입 체커가 무용지물이 된다.
unknown은 any 대신 쓸 수 있는 타입 시스템에 부합하는 타입이다.
- 어떠한 타입이든 unknown에 할당 가능하다.
- unknown은 오직 unkown과 any에만 할당 가능하다.
never 타입은 unknown과 정반대이다.
- 어떤 타입도 never에 할당할 수 없다.
- never는 어떠한 타입으로도 할당 가능하다.
unknown 타입인 채로 값을 사용하면 오류가 발생한다. 적절한 타입으로 변환하도록 강제할 수 있다.
const book = safeParseYAML(
`name: Jane Eyre
author: CharlotteBronte`
) as Book;
애초에 반환값이 Book이라고 기대하며 함수를 호출하기 때문에 단언문은 문제가 되지 않는다.
오히려 오류 정보가 unknown 타입일 때보다 더 정확하다.
2. 변수 선언과 관련된 unknown
타입 단언문이 유일한 방법은 아니다. instanceof를 체크한 후 unknown에서 원하는 타입으로 변환할 수 있다.
function processValue(val: unknown) {
if (val instanceof Date) {
val; // 타입이 Date
}
}
사용자 정의 타입 가드도 unknown에서 원하는 타입으로 변환할 수 있다.
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)) {
val; // 타입이 Book
}
}
unknown 타입의 범위를 좁히기 위해서는 상당히 많은 노력이 필요하다.
unknown 대신 제너릭 매개변수가 사용되는 경우도 있다.
function safeParseVAML<T>(yaml: string): T {
return parseYAML(yaml);
}
그러나 이는 일반적으로 타입스크립트에서 좋지 않은 스타일이다.
타입 단언문과 달라 보이지만 기능적으로는 동일하다.
제너릭보다는 unknown을 반환하고 사용자가 직접 단언문을 사용하거나 타입을 좁히도록 강제하는 것이 좋다.
3. 단언문과 관련된 unknown
이중 단언문에서 any 대신 known을 사용할 수도 있다.
declare const foo: Foo;
let barAny = foo as any as BarProp;
let barUnk = foo as unknown as Bar;
barAny와 barUnk는 기능적으로 동일하지만, 나중에 두 개의 단언문을 분리하는 리팩터링을 한다면 unknown이 더 안전하다.
any와 달리 unknown의 경우는 분리되는 즉시 오류를 발생하게 되므로 더 안전하다.
4. unknown과 유사하지만 조금 다른 타입들
object 또는 {}를 사용할 수도 있다.
unknown만큼 범위가 넓은 타입이지만, unknown보다는 범위가 약간 좁다.
- {} 타입은 null과 undefined를 제외한 모든 값을 포함한다.
- object 타입은 모든 비기본경 타입으로 이루어진다. boolean, number, string은 포함되지 않지만 객체와 배열은 포함된다.
unkown 타입이 도입되기 전에는 {}를 일반적으로 사용했다.
정말로 null과 undefined가 불가능하다고 판단되는 경우만 unknown 대신 {}를 사용하자.
43. 몽키 패치보다는 안전한 타입을 사용하기
자바스크립트의 가장 유명한 특징 중 하나는, 객체와 클래스에 임의의 속성을 추가할 수 있을 만큼 유연하다는 것이다.
window나 documen, DOM 엘리먼트, 내장 기능의 프로토타입 등에도 속성을 추가하거나 할 수 있다.
객체에 임의의 속성을 추가하는 것은 일반적으로 좋은 설계가 아니다.
windown 또는 DOM 노드에 데이터를 추가한다고 하면, 데이터는 기본적으로 전역 변수가 되며
전역 변수를 사용하면 은연중에 프로그램 내에서 멀리 떨어진 부분들 간에 의존성을 만든다.
그러면 함수를 호출할 때마다 사이드 이펙트를 고려해야 한다.
또 타입스크립트의 타입 체커는 Document의 내장 속성에 대해서는 알고 있지만, 임의로 추가한 속성에 대해서는 알지 못한다.
document.monkey = "Monkey";
// 'Document' 유형에 'monkey' 속성이 없습니다.
(document as any).monkey = "Monkey";
any 단언문을 사용하면 타입 체커는 통과하지만, 타입 안전성을 상실하고 언어 서비스를 사용할 수 없게 된다.
최선의 해결책은 document로부터 데이터를 분리하는 것이다.
만약 분리할 수 없는 경우라면 두 가지 차선책이 있다.
1. interface의 특수 기능 중 하나인 augmentation 사용
interface Document {
monkey: string;
}
document.monkey = "Monkey"; // 정상
모듈의 관점에서 제대로 동작하게 하려면 global 선언을 추가해야 한다.
export {};
declare global {
interface Document {
monkey: string;
}
}
보강을 사용할 때 주의할 점은 모듈 스코프다.
보강은 전역적으로 적용되기 때문에, 코드의 다른 부분이나 라이브러리로부터 분리할 수 없다.
애플리케이션이 실행되는 동안 속성을 할당하면 실행 시점에서 보강을 적용할 방법이 없다.
2. 더 구체적인 타입 단언문 사용
interface MonkeyDocument extends Document {
monkey: string;
}
(document as MonkeyDocument).monkey = "Monkey";
타입 단언문은 정상이며 할당문의 타입은 안전하다. Document를 건드리지 않았기 때문에 모듈 영역 문제도 해결할 수 있다.
그러나 몽키 패치를 남용해서는 안 되며 궁극적으로 더 잘 설계된 구조로 리팩터링 하는 것이 좋다.
44. 타입 커버리지를 추적하여 타입 안전성 유지하기
명시적 any 타입과 서드파티 타입 선언(@types)으로 인해 any 타입이 여전히 남아있을 수 있다.
npm의 type-coverage 패키지를 활용하여 any를 추적할 수 있는 방법이 있다.
실행할 때 --detail 플래그를 붙이면 any 타입이 있는 곳을 모두 출력해 준다.
서드파티 라이브러리로부터 비롯되는 any 타입의 가장 극단적인 예는 전체 모듈에 any 타입을 부여하는 것이다.
declare module "my-module";
my-module에서 어떤 것이는 오류 없이 임포트할 수 있지만, 임포트한 모든 심벌은 any타입이 된다.
any 타입이 사용되는 코드가 실제로는 더 이상 실행되지 않는 코드일 수 있다.
타입 커버리지를 추적하여 코드를 꾸준히 점검하자.