📢 어렵고 정석적인 개념 설명보다는 저같은 초보자도 이해하기 쉽게 정리하는 것을 원칙으로 포스팅하고 있습니다. 😄

[TypeScript] 타입 추론 : 타입 넓히기와 타입 좁히기

타입 넓히기

let num = 1;

위와 같이 숫자 1이 할당된 변수 num이 있다고 하자. 타입스크립트에서는 변수를 선언할 때 항상 변수 이름 옆에 타입을 쓴다고 했었다. 하지만, 모든 변수에 타입을 일일이 적는 것도 힘들고 번거로울 수 있기 때문에 타입스크립트는 변수의 초깃값을 기준으로 타입을 자동적으로 추론하는 기능을 제공해 준다. 

 

그렇다면 자동으로 타입이 추론된 변수 num의 타입은 무엇일까? 바로 number 타입이다. 하지만, 일부는 이런 생각을 할 수도 있다. "숫자 1을 넣었으니까 숫자 타입이 아닌 숫자 리터럴 타입으로 추론되는 게 맞지 않나?" 물론, 이 말도 맞지만 타입스크립트의 타입 추론은 기본적으로 타입 넓히기에 입각하여 추론된다. 타입 넓히기란 타입을 추론할 때 좁은 타입(ex. 리터럴 타입)이 아닌 넓은 타입(ex. 기본 타입)으로 추론되는 것을 의미하며 특히 let으로 선언된 변수의 경우 기본 타입으로 추론된다.

 

리터럴 타입으로 추론되는 경우도 있다. 바로 const로 선언하는 경우이다. 어차피 상수이기 때문에 재할당이 불가능하므로 타입스크립트는 const 변수의 경우 리터럴 타입으로 추론한다.

let abc;

초깃값이 아무것도 없는 변수는 타입스크립트가 무슨 타입으로 추론할까? 정답은 any 타입이다.

any 타입이기 때문에 숫자나 문자열 등 모든 타입의 값이 들어올 수 있다. 또한, 변수가 any 타입으로 추론될 경우엔 특별한 특징을 갖게 된다.

let abc; // any

abc = 1; // any -> number
abc.toFixed(); // number

abc = 'hello'; // any -> string
abc.toUpperCase(); // string

any 타입으로 추론된 변수에 특정 타입의 값이 들어올 때마다 any 타입은 카멜레온처럼 그 값의 타입으로 변하게 된다.

위 코드를 보면 abc 변수에 1이 들어오자마자 abc는 number 타입이 되고, 다시 'hello'가 들어오자마자 string 타입으로 변한 것을 볼 수 있다. 처음부터 any 타입으로 아예 선언된 경우에는 타입이 값에 따라 변하지는 않는다. 하지만, any 타입으로 추론된 경우에는 할당되는 값에 따라 변수의 타입이 변하게 된다.

 

합집합과 교집합

type Type1 = {
    name: string;
    color: string;
}

type Type2 = {
    name: string;
    location: string;
}

type Union = Type1 | Type2; // 합집합
type Intersection = Type1 & Type2; // 교집합

// 합집합 : Type1 또는 Type2 또는 Type1 + Type2
let abc: Union = {
    name: 'shawn',
    color: 'red'
    // OR
    name: 'shawn',
    location: 'seoul'
    // OR
    name: 'shawn',
    color: 'red',
    location: 'seoul'
}

// 교집합 : Only Type1 + Type2
let abc: Intersection = {
    name: 'shawn',
    color: 'red',
    location: 'seoul'
}

타입을 넓히는 방법으로 타입 간 합집합교집합을 사용할 수도 있다.

위 코드처럼 Type1과 Type2라는 두 개의 타입이 있을 때 합집합으로 합칠 경우 Type1 혹은 Type2 혹은 Type1 + Type2의 프로퍼티를 모두 사용할 수 있다. 타입에서 합집합이란 'A타입을 쓸 수도 있고, B타입을 쓸 수도 있고 아니면 전부 쓸 수도 있고'의 개념이라서 어느 하나의 타입으로 고정시키기보다는 여러 타입을 능동적으로 사용하고 싶을 때 사용한다.

 

반대로, 교집합의 경우에는 A타입과 B타입 모두를 합치고 싶을 때 사용한다. 합친다는 개념에서 합집합과 헷갈릴 수 있다.

교집합이라는 것 자체가 두 집합 사이의 공통된 값의 집합이기 때문에 A타입과 B타입이 모두 있는 게 공통된 값의 집합이라고 할 수 있다.

 

합집합과 교집합 개념에서 헷갈리는 포인트가 아마 합집합인데 교집합 같고 교집합인데 합집합 같다는 점일 것이다.

이렇게 헷갈리는 이유가 바로 타입을 덩어리로 보는 게 아니라 타입 안에 있는 프로퍼티를 중심으로 봐서 그렇다.

A타입 안에 있는 프로퍼티와 B타입 안에 있는 프로퍼티를 기준으로 합집합과 교집합을 바라보면 안 된다. 타입 안에 있는 프로퍼티들은 모두 분리될 수 없는 한 세트라고 보면 된다. (A세트와 B세트를 비교해야지 A세트의 구성품 a와 B세트의 구성품 a를 자꾸 비교해서는 안 된다.)

 

타입 좁히기

function test(value: number | string | boolean) {
    value; // union 타입

    if (typeof value === 'number') { // value는 number 타입이 된다.
        console.log(value.toFixed()); 
    }
}

다음과 같은 함수가 있다고 하자. 매개변수 value는 number, string, boolean 세 가지 타입의 union 타입으로 설정되어 있다. 하지만, 조건문으로 value의 타입이 number라고 가정하는 순간 그 조건문 안의 value 타입은 number 타입이 된다.

이렇게 여러 타입 중 하나의 타입을 딱 정하는 것을 타입 좁히기라고 한다. 또한, 위의 조건문처럼 타입을 좁혀주는 식을 타입 가드라고 한다. union 타입이 타입 가드를 만나면 타입이 하나로 좁혀진다.

 

타입을 좁히는 방법에는 typeof 조건문만이 아니라 instanceof, in 연산자 등 다양한 방법이 있다.

무엇을 쓰던 정답은 없다. 예를 들어, a라는 변수의 타입이 string | null이라면, if (a)를 줘서 a의 타입을 string으로 좁힐 수도 있고, b라는 변수의 타입이 number | string | Person이라면, if ('age' in b)를 줘서 b의 타입을 Person이라는 객체 타입으로 좁힐 수도 있다.

 

즉, 타입을 좁히는 방법은 정해져 있는 것이 아니라 좁힐 타입에 맞춰서 연산자 등을 이용해 조건식을 만들면 된다.

 

1. let num = 1; // 자동으로 number 타입으로 추론된다.
2. const num = 1; // 어차피 상수이기 때문에 리터럴 타입으로 추론된다.
3. 타입 객체 A | 타입 객체 B (합집합) = A 혹은 B 혹은 A + B
4. 타입 객체 A & 타입 객체 B (교집합) = A + B
5. 조건문(typeof, instanceof, in 등..)을 통해 union 타입을 하나의 타입으로 타입 좁히기 가능