타입 추론
타입 추론 (Type Inference)
TypeScript에서는 명시적인 타입 주석이 없을 때 타입 정보를 채워 주는 데 타입 추론을 쓰는 곳이 여럿 있어요. 변수와 멤버를 초기화할 때, 매개변수의 기본값을 정할 때, 그리고 함수의 반환 타입을 결정할 때 이런 추론이 일어나죠. 이 글에서는 추론이 어떻게 이루어지는지, 그리고 그 과정에서 생기는 미묘한 부분들을 살펴볼게요.
출처: TypeScript 핸드북
가장 좋은 공통 타입(Best common type)
여러 표현식에서 타입 추론이 이루어질 때는, 그 표현식들의 타입을 이용해 '가장 좋은 공통 타입'을 계산해요. 예를 들어:
let x = [0, 1, null];
// ^?
위 예시에서 x의 타입을 추론하려면 배열의 각 요소 타입을 고려해야 해요. 여기서 배열의 타입으로 선택지는 두 가지, number와 null이 있죠. 가장 좋은 공통 타입 알고리즘은 각 후보 타입을 살펴보고, 다른 모든 후보들과 호환되는 타입을 골라요.
가장 좋은 공통 타입은 제공된 후보 타입들 중에서 골라야 하기 때문에, 타입들이 공통 구조를 공유하지만 어떤 단일 타입도 모든 후보의 상위 타입이 되지 못하는 경우가 있어요. 예를 들어:
// @strict: false
class Animal {}
class Rhino extends Animal {
hasHorn: true;
}
class Elephant extends Animal {
hasTrunk: true;
}
class Snake extends Animal {
hasLegs: false;
}
// ---cut---
let zoo = [new Rhino(), new Elephant(), new Snake()];
// ^?
이상적으로는 zoo가 Animal[]로 추론되길 원할 텐데, 배열 안에 정확히 Animal 타입인 객체가 없기 때문에 배열 요소 타입에 대해 추론을 하지 못해요. 어떤 단일 타입도 다른 모든 후보의 상위 타입이 아닐 때는 타입을 명시적으로 지정해 주면 돼요:
// @strict: false
class Animal {}
class Rhino extends Animal {
hasHorn: true;
}
class Elephant extends Animal {
hasTrunk: true;
}
class Snake extends Animal {
hasLegs: false;
}
// ---cut---
let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()];
// ^?
가장 좋은 공통 타입을 찾지 못하면, 그 결과로 합집합 배열 타입 (Rhino | Elephant | Snake)[]이 추론돼요.
문맥적 타이핑(Contextual Typing)
타입 추론은 어떤 경우에는 '반대 방향'으로도 이루어져요. 이걸 '문맥적 타이핑'이라고 불러요. 문맥적 타이핑은 표현식의 타입이 그 위치로부터 암시될 때 일어나요. 예를 들어:
// @errors: 2339
window.onmousedown = function (mouseEvent) {
console.log(mouseEvent.button);
console.log(mouseEvent.kangaroo);
};
여기서 TypeScript 타입 체커는 할당문 오른쪽에 있는 함수 표현식의 타입을 추론하기 위해 Window.onmousedown 함수의 타입을 사용했어요. 그 과정에서 mouseEvent 매개변수의 타입까지 추론할 수 있었죠. 실제로 그 타입에는 button 프로퍼티가 있지만 kangaroo 프로퍼티는 없어요.
이게 가능한 이유는 window에 이미 onmousedown이 선언되어 있기 때문이에요:
// Declares there is a global variable called 'window'
declare var window: Window & typeof globalThis;
// Which is declared as (simplified):
interface Window extends GlobalEventHandlers {
// ...
}
// Which defines a lot of known handler events
interface GlobalEventHandlers {
onmousedown: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
// ...
}
TypeScript는 다른 문맥에서도 타입을 추론할 만큼 똑똑해요:
// @errors: 2339
window.onscroll = function (uiEvent) {
console.log(uiEvent.button);
};
위 함수가 Window.onscroll에 할당되는 사실을 바탕으로, TypeScript는 uiEvent가 이전 예시의 MouseEvent가 아니라 UIEvent라는 걸 알아내요. UIEvent 객체에는 button 프로퍼티가 없으므로, TypeScript는 에러를 던져요.
만약 이 함수가 문맥적 타이핑이 적용되는 위치가 아니라면, 함수의 인자는 암시적으로 any 타입을 갖고 에러를 내지 않아요 (noImplicitAny 옵션을 켜지 않는 한):
// @noImplicitAny: false
const handler = function (uiEvent) {
console.log(uiEvent.button); // <- OK
};
또한 함수 인자에 타입 정보를 명시적으로 부여해서 문맥적 타입을 덮어쓸 수도 있어요:
window.onscroll = function (uiEvent: any) {
console.log(uiEvent.button); // <- Now, no error is given
};
다만 uiEvent에는 button이라는 프로퍼티가 없으므로, 이 코드는 undefined를 로그로 출력할 거예요.
문맥적 타이핑은 많은 경우에 적용돼요. 흔한 경우로는 함수 호출의 인자, 할당문의 오른쪽, 타입 단언(type assertion), 객체 리터럴과 배열 리터럴의 멤버, 그리고 return 문이 있어요. 문맥적 타입은 가장 좋은 공통 타입의 후보 타입으로도 동작해요. 예를 들어:
// @strict: false
class Animal {}
class Rhino extends Animal {
hasHorn: true;
}
class Elephant extends Animal {
hasTrunk: true;
}
class Snake extends Animal {
hasLegs: false;
}
// ---cut---
function createZoo(): Animal[] {
return [new Rhino(), new Elephant(), new Snake()];
}
이 예시에서 가장 좋은 공통 타입은 Animal, Rhino, Elephant, Snake라는 네 개의 후보를 가지게 돼요. 그중에서 가장 좋은 공통 타입 알고리즘은 Animal을 선택할 수 있어요.