ts에서 타입 좁히기는 변수가 가질 수 있는 타입을 제한하여 더 구체적인 타입으로 좁히는 것을 말한다. 타입 좁히기는 주로 조건문이나 반복문 등에서 변수의 타입을 추론할 때 사용된다.
예를 들어, 아래와 같이 x 변수를 선언하고, if 문에서 x 변수의 타입을 좁히는 경우,
let x: string | number = "hello";
if (typeof x === "string") {
// x는 이 블록 내에서 string 타입으로 추론된다.
console.log(x.toUpperCase());
} else {
// x는 이 블록 내에서 number 타입으로 추론된다.
console.log(x.toFixed(2));
}
x 변수의 타입을 string 또는 number로 제한하여, if문 내에서 각각 다른 타입으로 사용할 수 있게 된다. 이렇게 타입을 좁히면, 코드 안정성을 높일 수 있다.
ts에서 타입 좁히기를 할 때는 주로 typeof, instanceof, in, null 체크 등을 사용한다. 이외에도, 함수의 반환 타입이나 인자 타입에 따라 변수의 타입을 추론하는 것도 가능하다.
null 체크
타입 체커는 일반적으로 이런 조건문에서 타입 좁히기를 잘하지만, 타입 별칭이 존재한다면 그러지 못할 수도 있다.
const el = document.getElementById('foo'); // 타입이 HTMLElement | null
if (el) { // 타입 좁히기
el // 타입이 HTMLElement
el.innerHTML = 'Party Time'.blink();
} else {
el // 타입이 null
alert('No element #foo');
}
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 // Type is A
} else {
ab // Type is B
}
ab // Type is A | B
}
변수의 값은 변경될 수 있지만, ts의 타입은 일반적으로 변경되지 않는다.
객체를 생성할 때는 속성을 하나씩 추가하기 보다는 여러 속성을 포함해서 한꺼번에 생성해야 타입 추론에 유리하다.
const pt = {}
pt.x = 3
// ~ Property 'x' does not exist on type '{}'
pt.y = 4
// ~ Property 'y' does not exist on type '{}'
pt 타입은 {} 을 기준으로 추론되기 때문에 오류가 발생한다.
이런 문제는 객체를 한번에 정의하면 해결할 수 있다.
interface Point {
x: number
y: number
}
const pt = {
x: 3,
y: 4,
} // OK
작은 객체들을 조합해서 큰 객체를 만들어야 하는 경우에는 스프레드 연산자 ... 를 사용하여 한꺼번에 만들 수 있다.
const pt = { x: 3, y: 4 }
const id = { name: 'Pythagoras' }
const namedPoint = { ...pt, ...id }
namedPoint.name // OK, type is string
객체 전개 연산자로 하나의 속성을 조건부 연산으로 추가하면 타입이 선택적 속성을 가진것으로 추론된다. 그러나 여러개의 속성을 추가하면 이는 유니온으로 추론된다. 이 경우 선택적 필드 방식으로 표현하려면 아래와 같이 헬퍼 함수를 사용하자.
(헬퍼 함수는 작은 기능을 수행하는 함수로, 코드의 재사용성을 높이고 가독성을 향상시키는 데 도움을 준다.)
// 헬퍼함수
const addOptional = <T extends object, U extends object>(a: T, b: U | null): T & Partial<U> => {
return { ...a, ...b };
};
declare let hasDates: boolean;
const nameTitle = { name: 'Khufu', title: 'Pharaoh' };
const pharaoh = addOptional(nameTitle, hasDates ? { start: -2589, end: -2566 } : null);
pharaoh.start // 정상, 타입이 number | undefined
가끔 객체나 배열을 변환해서 새로운 객체나 배열을 생성하려는 경우 내장된 함수형 기법 또는 로대시 같은 유틸리티 라이브러리를 사용하는 것이 ‘한꺼번에 객체 생성하기' 관점에서 보면 옳다.