38. any 타입은 가능한 한 좁은 범위에서만 사용하기
function processBar(b: Bar) {}
function expressionReturningFoo(): Foo {}
function f() {
const x = expressionReturningFoo();
processBar(x);
// 'Foo' 형식의 인수는 'Bar' 형식의 매개변수에 할당될 수 없습니다.
}
x가 foo 타입과 Bar 타입에 동시에 할당 가능하다면, 오류를 제거하는 방법은 두 가지다.
function f1() {
const x: any = expressionReturningFoo(); // 나쁜 방법
processBar(x);
}
function g() {
const foo = f1(); // 타입이 any
foo.fooMethod(); // 체크되지 않음
}
function f2() {
const x = expressionReturningFoo();
processBar(x as any); // 이게 낫다
}
f1의 경우 함수의 마지막까지 x의 타입이 any인 반면,
f2의 경우 processBar 호출 이후에 x가 그대로 Foo 타입이다.
any 타입이 processBar 함수의 매개변수에만 사용된 표현식이므로 다른 코드에는 영향을 미치지 않는다.
강제로 타입 오류를 제거하려면 any 대신 @ts-ignore를 사용하는 게 좋다.
function f() {
const x = expressionReturningFoo();
// @ts-ignore
processBar(x);
return x;
}
다음 줄의 오류가 무시된다. 그러나 근본적인 원인을 해결한 것이 아니기 때문에 더 큰 문제가 발생할 수도 있다.
객체의 속성이 타입 오류를 가질 때도, 객체 전체를 as any로 선언하기보다는 최소한의 범위에만 any를 사용하는 게 좋다.
함수의 반환 타입이 any인 경우 타입 안정성이 나빠지므로 절대로 any를 반환하면 안 된다.
39. any를 구체적으로 변형해서 사용하기
any는 자바스크립트에서 표현할 수 있는 모든 값을 아우르는 매우 큰 범위의 타입이다.
any를 사용할 때는 정말로 모든 값이 허용되어야만 하는지 면밀히 검토해야 한다.
일반적인 상황에서는 any보다 더 구체적으로 표현할 수 있는 타입이 존재할 가능성이 높다.
더 구체적인 타입을 찾아 타입 안전성을 높이도록 하자.
any 타입의 값을 그대로 정규식이나 함수에 넣는 것은 권장되지 않는다.
function getLengthBad(array: any) { // 이렇게 하지 말자
return array.length;
}
function getLength(array: any[]) {
return array.length;
}
getLength가 더 좋은 함수다.
- 함수 내의 array.length 타입이 체크된다.
- 함수의 반환 타입이 any 대신 number로 추론된다.
- 함수 호출될 때 매개변수가 배열인지 체크된다.
배열의 배열 형태라면 any[][]처럼, 객체이지만 값을 알 수 없다면 {[key: string]: any} 처럼 선언하면 된다.
{[key: string: any} 대신 모든 비기본형(non-primitive) 타입을 포함하는 object 타입을 사용할 수도 있다.
object 타입은 객체의 키를 열거할 수는 있지만 속성에 접근할 수는 없다.
function hasTwelveLetterKey1(o: { [key: string]: any }) {
for (const key in o) {
if (key.length === 12) {
console.log(key, o[key]);
return true;
}
}
return false;
}
function hasTwelveLetterKey2(o: object) {
for (const key in o) {
if (key.length === 12) {
console.log(key, o[key]);
// '{}' 형식에 인덱스 시그니처가 없으므로
// 오소에 암시적으로 'any' 형식이 있습니다.
return true;
}
}
return false;
}
함수의 타입에도 단순히 any를 사용해서는 안 된다. 최소한으로마나 구체화해보자.
type Fn0 = () => any; // 매개변수 없이 호출 가능한 모든 함수
type Fn1 = (arg: any) => any; // 매개변수 1개
type FnN = (...args: any[]) => any; // 모든 개수의 매개변수 ("Function" 타입과 동일)
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[]) {
// '(...args: any[]) => any' 형식은 'T' 형식에 할당할 수 없습니다.
if (!lastArgs || !shallowEqual(lastArgs, args)) {
lastResult = fn(...args);
lastArgs = args;
}
return lastArgs;
};
}
타입스크립트는 반환문에 있는 함수와 원본 함수 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 lastArgs;
} as unknown as T;
}
함수 내부에는 any가 꽤 많이 보이지만 타입 정의에는 any가 없기 때문에,
cacheLast를 호출하는 쪽에서는 any가 사용됐는지 알지 못한다.
괜찮게 구현되긴 했지만, 두 가지 문제점이 있긴 하다.
- 함수를 연속으로 호출하는 경우 this의 값이 동일한지 체크하지 않는다.
- 원본 함수가 객체처럼 속성 값을 가지고 있었다면 레퍼 함수에는 속성 값이 없기 때문에 타입이 달라진다.
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]) {
// '{}' 형식에 인덱스 시그니처가 없으므로
// 요소에 암시적으로 'any' 형식이 있습니다.
return false;
}
}
return Object.keys(a).length === Object.keys(b).length;
}
if 구문의 k in b 체크로 b 객체에 k 속성이 있다는 것을 확인했지만 b[k] 에서 오류가 발생하는 것이 이상하다.
어쨌든 실제 오류가 아니라는 것을 알고 있기 때문에 any로 단언하는 수밖에 없다.
function shallowObject<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;
}
b as any 타입 단언문은 k in b 체크를 했기 때문에 안전하다.
객체가 같은지 체크하기 위해 객체 순회와 단언문이 코드에 직접 들어가는 것보다, 예시처럼 별도의 함수로 분리해 내는 것이 좋다.
41. any의 진화를 이해하기
타입스크립트에서 일반적으로 변수의 타입은 변수를 선언할 때 결정된다.
그 후에 null 체크 등으로 정제될 수 있지만, 새로운 값이 추가되도록 확장할 수는 없다.
그러나 any 타입과 관련해서 예외인 경우가 존재한다.
function range(start: number, limit: number) {
const out = []; // any[]
for (let i = start; i < limit; i++) {
out.push(i); // any[]
}
return out; // number[]
}
out의 타입이 처음에는 any[]로 초기화되었는데, 마지막에는 number[]로 추론되고 있다.
number 타입의 값을 넣는 순간부터 타입은 number[]로 진화한다.
타입의 진화는 타입 좁히기와 다르다. 배열에 다양한 타입의 요소를 넣으면 배열의 타입이 확장되며 진화한다.
const result = []; // any[]
result.push("a");
result; // string[]
result.push(1);
result; // (string | number)[]
조건문에서는 분기에 따라 타입이 변할 수도 있다.
let val; // any
if (Math.random() < 0.5) {
val = /hello/;
val; // RegExp
} else {
val = 12;
val; // number
}
val; // number | RegExp
변수의 초깃값이 null인 경우도 any의 진화가 일어난다. 보통은 try/catch 블록 안에서 변수를 할당하는 경우에 나타난다.
let val = null; // any
try {
// somethingDangerous();
val = 12;
val; // number
} catch (e) {
console.warn("alas!");
}
val; // number | null
any 타입의 진화는 noImplicitAny가 설정된 상태에서 변수의 타입이 암시적 any인 경우에만 일어난다.
명시적으로 any를 선언하면 타입이 그대로 유지된다.
let val: any; // any
if (Math.random() < 0.5) {
val = /hello/;
val; // any
} else {
val = 12;
val; // any
}
val; // any
암시적 any 상태인 변수에 어떠한 할당도 하지 않고 사용하려고 하면 암시적 any 오류가 발생한다.
function range(start: number, limit: number) {
const out = [];
// 'out' 변수는 형식을 확인할 수 없는 경우
// 일부 위치에서 암시적으로 'any[]' 형식입니다.
if (start === limit) {
return out;
// 'out' 변수에는 암시적으로 'any[]' 형식이 포함됩니다.
}
}
any 타입의 진화는 암시적 any 타입에 어떤 값을 할당할 때만 발생한다.
어떤 변수가 암시적 any 상태일 때 값을 읽으려고 하면 오류가 발생한다.
암시적 any 타입은 함수 호출을 거쳐도 진화하지 않는다.
function makeSquares(start: number, limit: number) {
const out = []; // 'out' 변수는 일부 위치에서 암시적으로 'any[]' 형식입니다.
range(start, limit).forEach((i) => {
out.push(i * i);
});
return out; // 'out' 변수에는 암시적으로 'any[]' 형식이 포함됩니다.
}
forEach 안의 화살표 함수는 추론에 영향을 미치지 않는다.
루프로 순회하는 대신, 배열의 map과 filter를 통해 단일 구문으로 배열을 생성하여 any 전체를 진화시키는 방법을 생각해 볼 수 있다.
진화한 배열의 타입이 (string|number)[]라면, number[]이어야 하는데 실수로 string이 섞인 것일 수 있다.
타입을 안전하게 지키기 위해서는 암시적 any를 진화시키는 방식보다 명시적 타입 구문을 사용하는 것이 더 좋은 설계다.