
런타임에 모든 변수는 유일한 값을 가진다.
그러나 타입스크립트가 작성된 코드를 체크하는 정적 분석 시점에, 변수는 '가능한' 값들의 집합인 타입은 가진다.
따라서 타입을 명시하지 않으면 타입 체커가 지정된 값을 가지고 할당 가능한 값들의 집합을 유추해야 한다.
이 과정을 타입스크립트에서는 '넓히기' 라고 한다.
interface Vector3 {x:number; y:number; z: number;}
function getComponent(vector:Vector3, axis: 'x'| 'y'|'z') {
return vector[axis];
}
let x = 'x';
let vec = {x:10, y:10, z: 10};
getComponent(vec,x)
// x 인자에 오류 발생
위 코드에서 함수는 런타임에 오류없이 실행되지만, x의 타입이 할당 시점에 넓히기가 동작하여 string으로 추론되었기 때문에 axis의 타입과 맞지 않아 오류가 발생한다.
✔️ 타입 넓히기가 진행될 때, 주어진 값으로 추론 가능한 타입이 여러 개이기 때문에 과정이 상당히 모호하다. 정보가 충분하지 않다면 타입스크립트는 작성자의 의도를 추측한다.
하지만, 사람의 마음까지 읽은 순 없기 때문에 항상 옳진 않다.
➡️ 타입스크립트는 넓히기의 과정을 제어할 수 있도록 몇 가지 방법을 제공한다.
const x = 'x' // 타입은 'x'
let vec = {x:10, y:10, z: 10};
getComponent(vec, x) // 정상 작동
하지만 const 또한 객체나 배열의 경우에 여전히 문제가 발생한다.
const v = {
x:1,
}
v.x = 3;
v.x = '3';
v.y = 4;
v.name = 'Pythagoras';
위 예시는 자바스크립트에서는 작동하지만 타입스크립트에서는 v의 타입을 {x:number} 가 되기 때문에 string으로 재할당이나 다른 속성을 추가하지 못한다.
➡️ 타입 추론의 강도를 직접 제어하려면 타입스크립트의 기본 동작을 재정의해야 한다.
const v:{x:1|3|5} = {
x: 1;
} // 타입은 {x:1|3|5}
타입 체커에 추가적인 문맥을 제공한다. (ex. 함수의 매개변수로 값을 전달)
const 단언문을 사용한다.
변수 선언에 쓰이는 let, const와는 다른 것임을 인지하자.
const v1 = {
x:1,
y:2,
}
// 타입은 {x:number; y: number}
const v2 = {
x: 1 as const,
y: 2,
}
// 타입은 {x:1; y: number}
const v3 = {
x: 1,
y: 2,
} as const
// 타입은 {readonly x:1; readonly y: 2;}
const a1 = [1,2,3]; // 타입은 number[]
const a2 = [1,2,3] as const; // 타입은 readonly [1,2,3]
타입스크립트가 넓은 타입으로부터 좁은 타입으로 진행하는 과정을 이야기 한다.
가장 일반적인 예시는 null 체크이다.
const el = document.getElementById('foo'); // 타입은 HTMLElement | null
if(el) {
el; // 타입은 HTMLElement
el.innerHTML = 'Party'.blink();
} else {
el // 타입은 null
alert('adsf')
}
➡️분기문에서 예외를 던지거나 함수를 반환하여 블록의 나머지 부분에서 변수의 타입을 좁힐 수도 있다.
const el = document.getElementById('foo'); // 타입은 HTMLElement | null
if(!el) throw new Error('Unable');
el; // 타입은 HTMLElement
el.innerHTML = 'Party'
➡️instanceof을 사용해서 타입을 좁히는 방법도 있다.
function contains(text : string, search: string | RegExp) {
if(search instanceof RegExp) {
search; //타입은 RegExp
return !!search.exec(text);
}
search; //타입은 string
return text.includes(search);
}
➡️속성체크로도 타입을 좁힐 수 있다.
interface A {a:number}
interface B {b:number}
function pickAB(ab: A | B) {
if('a' in ab) {
ab // 타입은 A
} else {
ab // 타입은 B
}
ab // 타입은 A | B
}
➡️ Array.isArray 같은 일부 내장 함수로도 타입을 좁힐 수 있다.
function contains(text: string, terms: string | string[]) {
const termList = Array.isArray(terms) ? terms : [terms];
termList // 타입은 string []
}
타입스크립트는 위 예시들과 같이 일반적으로 조건문에서 타입을 좁히는 데 매우 능숙하다.
하지만 타입을 섣불리 판단하는 실수가 일어날 수 있으니 주의해야 한다.
const el = document.getElementById('foo'); // HTMLElement | null
if(typeof el === 'object') {
el; // HTMLElement | null
}
// 자바스크립트에서 typeof null 은 'object' 이므로 null이 제외되지 않는다.
function foo(x?: number | string | null) {
if(!x) {
x; // string | number | null | undefined
}
}
// 빈 문자열 '' 와 0 은 모두 false가 되기 떄문에 타입은 전혀 좁혀지지 않는다.
➡️ 타입을 좁히는 다른 일반적인 방법은 명시적 태그를 붙이는 것이다.
interface UploadEvent {type: 'upload'; fileName: string; contents: string};
interface DownloadEvent {type:'download'; filename: string};
type AppEvent = UploadEvent | DownloadEvent;
function handleEvent(e: AppEvent) {
switch(e.type) {
case 'download' :
e // 타입은 DownloadEvent
break;
case 'upload' :
e // 타입은 UploadEvent
break
}
}
위 패턴은 태그된 유니온 또는 구별된 유니온이라고 불린다.
➡️ 만약 타입스크립트가 타입을 식별하지 못한다면, 커스텀 함수를 도입할 수도 있다.
function isInputElement(el: HTMLElement): el is HTMLInputElement {
return 'value' in el;
}
function getElementContent(el: HTMLElement) {
if (isInputElement(el)) {
el; // 타입은 HTMLInputElement
return el.value;
}
el; // 타입은 HTMLElement
return el.textContent;
}
이러한 기법을 사용자 정의 타입 가드라고 한다.
el is HTMLInputElement 는 함수의 반환이 true인 경우. 타입 체커에게 매개변수의 타입을 좁힐 수 있다 알려준다.
어떤 함수는 타입 가드를 사용해 배열과 객체의 타입 좁히기를 할 수 있다.
const jackson5 = ['A','B','C','D','E'];
const members = ['df','eq'].map(
who => jackson5.find(n => n === who)
)
// 타입은 (string| undefined) []
const members1 = ['df','eq'].map(
who => jackson5.find(n => n === who)
).filter(who => who !== undefined);
// 타입은 (string| undefined) []
//타입 가드
function isDefined<T>(x: T | undefined): x is T {
return x !== undefined;
}
const members2 = ['df', 'eq'].map(
who => jackson5.find(n => n === who)
).filter(isDefined);
// 타입은 string []