- 외부로 드러나는 타입 정의는 간단하지만 내부 로직이 복잡해서 안전한 타입으로 구현하기 어려운 경우
- 이상적: 함수의 모든 부분을 안전한 타입으로 구현
- 단, 예외 상황까지 고려하며 타입 정보를 힘들게 구성할 필요는 없다.
- 함수 내부에는 타입 단언을 사용하고 함수 외부로 드러나는 타입 정의를 정확히 명시하는 정도로 끝내는 것이 나음
- 프로젝트 전반에 위험한 타입 단언문이 드러나 있는 것보다, 제대로 타입이 정의된 함수 안으로 타입 단언문을 감추는 것이 더 좋은 설계
<함수 캐싱: 첫 번째 예제>
declare function cacheLast<T extends Function>(fn: T): T;
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[]) {
// ~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type '(...args: any[]) => any' is not assignable to type 'T'
if (!lastArgs || !shallowEqual(lastArgs, args)) {
lastResult = fn(...args);
lastArgs = args;
}
return lastResult;
};
}
- 타입스크립트는 반환문에 있는 함수와 원본 함수 T타입이 어떤 관련이 있는지 알지 못하기 때문에 오류 발생
- 원본 함수 T타입과 동일한 매개변수로 호출되고 반환값 역시 예상한 결과이기 때문에 타입 단언문 추가로 오류를 제거해도 괜찮다.
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;
}
- 함수 내부에는 any가 많이 보이지만 함수 실행과 반환값에서 타입을 나타내고 있어(타입 정의에는 any가 없음) casheLast를 호출하는 쪽에서는 any가 사용되었는지 모름
<두 번째 예제>
declare function shallowEqual(a: any, b: any): boolean;
declare function shallowObjectEqual<T extends object>(a: T, b: T): boolean;
- 객체를 매개변수로 하는 shallowObjectEqual은 타입 정의는 간단하지만 구현이 shallowEqual에 비해 복잡함
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[k]) {**//k가 b에 있음을 체크 했지만 오류발생**
// ~~~~ Element implicitly has an 'any' type
// because type '{}' has no index signature
return false;
}
}
return Object.keys(a).length === Object.keys(b).length;
}
- 객체 매개변수 a와 b가 동일한 키를 가진다는 보장이 없기 때문에 구현할 때 주의
- 타입스크립트의 문맥 활용 능력이 부족해서 오류발생
- 실제 오류가 아님 ⇒ 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]) {//k in b를 체크하여 타입단언문은 안전
return false;
}
}
return Object.keys(a).length === Object.keys(b).length;
}
- b as any 타입 단언문은 k in b 체크를 했으므로 안전하다. → 정확한 타입으로 정의됨
- 객체가 같은지 체크하기 위해 객체 순회와 단언문이 코드에 직접 들어가는 것 보다 별도의 함수로 분리하는 것이 훨씬 좋은 설계
요약
- 타입 선언문은 일반적으로 타입을 위험하게 만들지만 상황에 따라 필요하기도 하고 현실적인 해결책이 되기도 함. 불가피하게 사용해야 한다면 정확한 정의를 가지는 함수 안으로 숨기자
'책 정리 > 이펙티브 타입스크립트' 카테고리의 다른 글
아이템 41 any의 진화를 이해하기 (0) | 2023.07.26 |
---|---|
아이템 26 타입 추론에 문맥이 어떻게 사용되는지 이해하기 (0) | 2023.07.26 |
아이템 25 비동기 코드에는 콜백 대신 async 함수 사용하기 (0) | 2023.07.26 |
아이템 14 타입 연산과 제너릭 사용으로 반복 줄이기 (0) | 2023.07.26 |
아이템 13 타입과 인터페이스의 차이점 알기 (0) | 2023.07.26 |