변수를 초기화할 때 타입을 명시하지 않으면 타입체커는 타입을 결정해야 한다. 이 때 타입스크립트는 지정된 단일 값을 가지고 할당 가능한 값들의 집합을 유추하는데 이 과정을 타입 넓히기
라고 한다.
다음 예제는 3D 벡터에 대한 타입과 그 요소들의 값을 얻는 함수이다.
interface Vector3 { x: number; y: number; z: number}
functino getComponent(vector: Vector3, axis: 'x' | 'y' | 'z') {
return vector[axis]
}
let x = 'x';
let vec = {x: 10, y: 20, z: 30}
getComponent(vec, x); // ❗️string 형식의 인수는
// 'x' | 'y' | 'z' 형식의 매개변수에 할당될 수 없습니다.
Vector3
를 사용한 함수는 런타임에 오류 없이 실행되지만 편집기에서는 오류가 표시된다.
이는 getComponent
함수는 두번째 매개변수로 union type
을 기대했으나 실제로 할당된 값인 x는 타입 넓히기에 의해 string
타입으로 추론되었기 때문이다.
이처럼 타입 넓히기가 진행될 때는 주어진 값으로 추론 가능한 타입이 여러개이므로 그 과정이 모호하다.
const mixed = ['x', 1]
위 코드에서 mixed
는 어떤 타입으로 추론될 수 있을까?
mixed
의 타입은 위에 적힌 타입 말고도 any 등 더 많은 타입으로 추론될 수 있다. 즉 정보가 충분하지 않을 경우 mixed
가 어떤 타입으로 추론되어야 할지 알 수 없다.
이러한 타입 넓히기 과정을 제어하기 위해 몇 가지 방법을 사용할 수 있다.
let 대신 const를 사용해 변수를 선언할 경우 선언과 동시에 값이 할당되고 재할당이 불가능하므로 타입스크립트는 좁은 타입으로 추론할 수 있다.
const x = 'x'
getComponent(vec, x)
위 코드에서 x는 const 로 선언되어 타입이 ‘x’로 추론되므로 처음과 같은 오류는 발생하지 않게 된다.
const가 모든 문제를 해결할 수 있었다면 정말 좋았겠지만 아쉽게도 const를 사용해도 객체와 배열에서는 여전히 문제가 존재한다.
const v = {
x: 1;
}
v.x = 2;
v.x = '2'; //❗️'2'는 number 형식에 할당할 수 없습니다.
v.y = 3; // ❗️ {x: number} 에 'y' 속성이 없습니다.
객체의 경우 타입스크립트의 넓히기 알고리즘은 각 요소를 let으로 할당된 것처럼 다룬다. 위 코드에서 v의 타입은 {x: number}
가 되며 그렇기 때문에 v.x
는 다른 숫자로 재할당할 수 있지만 string은 할당이 불가능하며 다른 속성을 추가할 수도 없다.
이와 같은 경우 타입체커에게 타입을 더 정확하게 추론할 수 있는 추가적인 문맥을 제공하여 타입 오류를 해결할 수 있다.
const 단언문은 타입 공간 기법 중 하나로 값 뒤에 as const를 작성하면 타입스크립트는 최대한 좁은 타입으로 추론하게 된다.
const v = {
x: 1 as const
y: 2
}
위 코드처럼 const 단언문을 사용하게 되면 타입체커는 해당 객체의 타입을 {x: 1; y: number}
와 같이 추론한다.
타입 좁히기는 타입스크립트가 넓은 타입으로부터 좁은 타입으로 진행하는 과정을 말한다.
타입스크립트에서는 몇 가지 방법을 사용해서 타입 좁히기를 진행할 수 있다.
const el = document.getElementById('foo') ; // type is HTMLElement | null
if(el) { // type is HTMLElement
el
el.innerHTML = 'Party Time'.blick()
} else { // type is null
alert('No elemnet #foo')
}
위 코드처럼 조건문을 사용해 null 체크를 진행할 수 있으며 타입 체커는 일반적으로 이러한 조건문에서 타입 좁히기를 잘 해낸다. 위와 같은 방법 이외에도 분기문에서 예외를 던지거나 함수를 반환하여 블록의 나머지 부분에서 변수의 타입을 좁힐 수도 있다.
instanceof
를 사용해서 타입을 좁힐 수도 있다.
function contains(text: string, search: string | RegExp) {
if(search instanceof RegExp) {
search // type is RegExp
return !!search.exec(text);
}
search // type is string
return text.includes(search)
}
속성 체크를 사용해서 타입을 좁힐 수도 있다.
interface A { a: number }
interface B ( b: number }
function pickAB(ab: A | B){
if('a' in ab){
ab // type is A
} else {
ab // type is B
}
ab // type is A | B
}
타입 좁히기를 사용할 때는 타입을 섣불리 판단하는 실수를 저지르지 않도록 조심해야 한다.
const el = document.getElementById('foo'); //type is HTMLElement | null
if(typeof el ==='object'){
el; // type is HTMLElement | null
}
}
위 코드는 null 을 제거하기 위해 작성한 코드이지만 자바스크립트에서 typeof null은 ‘object’이므로 의도와는 달리 실제로는 null이 제거되지 않았다.
명시적 태그를 붙임으로서 타입을 좁힐 수 있다.
interface UploadEvt{ type: 'upload'; filename: string; contents: string}
interface DownloadEvt{ type: 'download'; filename: string; contents: string}
type AppEvt = UploadEvt | DownloadEvt
function handleEvt(e: AppEvt){
switch(e.type) {
case 'download';
e // type is DownloadEvt
break;
case 'upload';
e // type is UploadEvt
break;
}
}
위와 같이 작성된 타입 패턴은 태그된 유니온 또는 구별된 유니온이라고 부른다.
function isInputElement(el: HTMLElement) : el is HTMLInputElement {
return 'value' in el;
}
function getElementContent(el: HTMLElement){
if(isInputElement(el)) {
el; // type is HTMLInputElement
reutrn el.value
}
el; // type is HTMLElement
return el.textContent
}
위와 같은 기법을 사용자 정의 타입 가드라고 한다. 반환 타입의 el is HTMLInputElement
는 함수의 반환이 true일 경우 타입 체커에게 매개변수의 타입을 좁힐 수 있다고 알려준다.
위 방법 이외에도 타입 가드를 사용하여 타입 좁히기 과정을 원할하게 만들 수 있다.
타입 가드는 내가 지금 잘 모르는 개념이라 추후 타입 가드에 대해 정리한 후 타입 가드로 타입 좁히기를 구현하는 예제도 다시 한 번 정리할 예정이다.
<이펙티브 타입스크립트> Dan Vanderkam, 프로그래밍 인사이트 (2021)