JS를 TS로 마이그레이션 할 때 any타입이 중요한 역할을 한다.
아이템38
any타입은 가능한 한 좁은 범위에서만 사용하기interface Foo {
foo: string;
}
interface Bar {
bar: string;
}
declare function expressionReturningFoo(): Foo;
function processBar(b: Bar) {
/* ... */
}
// BAD
function f1() {
const x: any = expressionReturningFoo();
processBar(x);
return x;
}
// BETTER
function f2() {
const x = expressionReturningFoo();
processBar(x as any);
return x;
}
f1의 경우 함수의 마지막까지 x의 타입이 any. 심지어 any타입을 리턴해 프로젝트 전반에 악영향을 끼친다.
f2의 경우 processBar 호출시에만 any. any타입의 사용 범위가 좁다.
아이템39
any를 구체적으로 변형해서 사용하기// BAD
function getLength(array: any) {
return array.length;
}
// BETTER
function getLength(array: any[]) {
return array.length;
}
// 더 나은 3가지 이유
// 1. array.length 타입이 체크 가능
// 2. 함수의 반환 타입이 number
// 3. 함수 호출 시 매개변수가 배열인지 체크 가능
function hasTwelveLetterKey(o: { [key: string]: any }) {
for (const key in o) {
if (key.length === 12) {
return true;
}
}
return false;
}
function hasTwelveLetterKey(o: object) {
for (const key in o) {
if (key.length === 12) {
console.log(key, o[key]);
// ~~~~~~ Element implicitly has an 'any' type
// because type '{}' has no index signature
return true;
}
}
return false;
}
object 타입은 객체의 속성 접근이 불가능하다.
// BAD
type Fn = any;
// BETTER
type Fn0 = () => any; // 매개변수 없음
type Fn1 = (arg: any) => any; // 매개변수 1개
type FnN = (...args: any[]) => any; // 모든 개수의 매개변수 (Function 타입과 동일)
const numArgsBad = (...args: any) => args.length; // Returns any
const numArgsGood = (...args: any[]) => args.length; // Returns number
any를 더 정확하게 모델링하자. 예를 들면 any[]
, { [key: string]: any }
, () => any
아이템40
함수 안으로 타입 단언문 감추기declare function shallowEqual(a: any, b: any): boolean;
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;
}
cacheLast를 호출하는 쪽에서는 any가 사용됐는지 알지 못한다.
declare function shallowEqual(a: any, b: any): boolean;
function shallowObjectEqual<T extends object>(a: T, b: T): boolean {
for (const [k, aVal] of Object.entries(a)) {
if (!(k in b) || aVal !== (b as any)[k]) {
return false;
}
}
return Object.keys(a).length === Object.keys(b).length;
}
k in b
를 체크 했으므로 b as any
는 안전합니다.
타입 단언문은 일반적으로 위험하지만 상황에 따라 필요하며 현실적인 해결책이 되기도 한다.
아이템41
any의 진화를 이해하기any 타입의 진화는 noImplicitAny가 설정된 상태에서 변수의 타입이 암시적 any인 경우에만 일어난다.
function range(start: number, limit: number) {
const out = []; // Type is any[]
for (let i = start; i < limit; i++) {
out.push(i); // Type of out is any[]
}
return out; // Type is number[]
}
const result = []; // Type is any[]
result.push('a');
result; // Type is string[]
result.push(1);
result; // Type is (string | number)[]
any 타입의 진화는 암시적 any 타입에 어떤 값을 할당할 때만 발생한다.
다음과 같이 함수 호출은 any를 진화시키지 않는다.
// 값을 할당해 any가 진화
function range(start: number, limit: number) {
const out = []; // Type is any[]
for (let i = start; i < limit; i++) {
out.push(i); // Type of out is any[]
}
return out; // Type is number[]
}
// 함수 호출로는 any가 진화하지 않는다
function makeSquares(start: number, limit: number) {
const out = [];
// ~~~ Variable 'out' implicitly has type 'any[]' in some locations
range(start, limit).forEach((i) => {
out.push(i * i);
});
return out;
// ~~~ Variable 'out' implicitly has an 'any[]' type
}
아이템42
모르는 타입의 값에는 any 대신 unknown을 사용하기unknown은 any의 첫 번째 속성만 만족, never는 두 번째 속성만 만족한다
function safeParseYAML(yaml: string): unknown {
return parseYAML(yaml);
}
const book = safeParseYAML(`
name: Villette
author: Charlotte Brontë
`) as Book;
// 반환값이 Book이라고 기대하며 함수를 호출하므로 단언문은 문제가 되지 않음
unknown 타입을 좁히기 위해서는 상당히 많은 노력이 필요하다.
in 연사자를 쓰기 전에 객체임을 확인해야 하며 typeof null
이 object이므로 별도로 null이 아님을 확인해야 한다.
// instanceof 사용
function processValue(val: unknown) {
if (val instanceof Date) {
val; // Type is Date
}
}
// 사용자 정의 타입 가드 사용
function isBook(val: unknown): val is Book {
return (
typeof val === 'object' && val !== null && 'name' in val && 'author' in val
);
}
function safeParseYAML<T>(yaml: string): T {
return parseYAML(yaml);
}
// 제너릭의 사용 스타일이 타입 단언문과 기능적으로 동일하다..
// 이 경우 제너릭보다는 unknown을 반환해 사용자가 직접 단언하거나 타입을 좁히도록 강제할 수 있다.
분리되는 즉시 오류를 발생하므로 더 안전하다
let barAny = foo as any as Bar;
let barUnk = foo as unknown as Bar;
{}
타입은 null과 undefined를 제외한 모든 값을 포함한다object
타입은 모든 비기본형(non-primitive) 타입으로 이루어진다정말로 null과 undefined가 불가능하다고 판단될 때만 {}
타입을 사용하자
아이템43
몽키 패치보다는 안전한 타입을 사용하기몽키패치의 정의가 뭘까?
// any 단언 사용
(document as any).monky = 'Tamarin';
// 인터페이스 보강 사용
interface Document {
/** 보강을 사용하면 이렇게 속성에 주석을 붙일 수 있다 */
monkey: string;
}
document.monkey = 'Tamarin';
보강은 전역적이기 때문에 어떤 경우에는 속성이 있고, 어떤 경우에는 속성이 없는 경우에는 문제가 된다.
이러한 이유로 속성을 monkey: string | undefined
와 같이 선언할 수 있지만, 다루기에 불편하다
interface MonkeyDocument extends Document {
monkey: string;
}
(document as MonkeyDocument).monkey = 'Macaque';
Document 타입을 건드리지 않고 확장했으므로, 모듈 영역 문제를 해결할 수 있다.
하지만 몽키패치를 남용하지 말고 궁극적으로는 더 좋은 구조로 리팩터링 하는 것이 좋다.
아이템44
타입 커버리지를 추적하여 타입 안전성 유지하기noImplicitAny
를 설정해도 프로그램 내에 any가 존재할 수 있다any[]
나 {[key: string]: any}
같은 타입은 인덱스를 생성하면 단순 any가 되고 코드 전반에 영향을 미친다.npx type-coverage
패키지를 통해 any를 추적할 수 있다npx type-coverage --detail
을 통해 any타입이 있는 곳을 모두 출력해볼 수 있다.
명시적 any를 사용한 경우
function getColumnInfo(name: string): any {
return utils.buildColumnInfo(appState.dataSchema, name);
}
서드파티 라이브러리로부터 비롯된 any