[타입추론 21] 타입 넓히기

2022. 7. 22. 11:43책/이펙티브 타입스크립트

1. 넓히기widening
타입이란 '할당 가능한 값들의 집합'이다. 그리고 변수를 초기화할 때(const x = 10; 처럼) 타입을 명시하지 않으면 타입 체커는 변수에 할당된 값을 통해 '할당 가능한 값들의 집합'을 유추하는데(즉, 타입을 추론하는데) 이 과정을 '넓히기(widening)'라고 부른다.

사실 주어진 값 하나만으로 타입 추론을 하다보면 가능한 타입이 여러 개라 상당히 모호해지고 어려운 작업이 된다 ('오늘 뭐먹을래?' '아무거나' '????????')

따라서 넓히기 과정속에서 오류의 원인이 파생되고, 이를 이해하면 이후 타입 구문을 더 효과적으로 사용할 수 있을 것이다.

2. 넓히기 제어
타입 추론이 얼마나 모호한 작업이 될 수 있는지 아래 예제를 보자.
const mixed = ['x', 1];

이 변수의 '할당 가능한 집합', 즉 어떤 타입이 될 수 있을까?

타입 후보군
 - ('x', 1) []
 - [ 'x', 1 ]
 - [string, number]
 - readonly [string, number]
 - (string | number) []
 - readonly (string | number) []
 - [any, any]
 - any[]

TS는 작성자의 의도를 추축하고, (string | number) []로 할당한다. 최대한 명확성과 유연성의 균형을 유지하려고 하는 것이다. 그러나 TS가 아무리 영리하다 한들 사람의 마음까지 읽을 수는 없는 노릇이다.

그리고 이러한 넓히기 과정을 제어하는 방법들은 아래와 같다.

 

2.1 const

  • 변수를 let대신 const로 선언하면 더 좁은 타입으로 추론된다
  • 예를들어, let x = 'x'로 선언하면 string으로 추론되지만, const x = 'x'는 'x' 인 리터럴 타입으로 추론된다
  • 왜냐하면 let은 재할당이 가능하므로 TS입장에선 값이 변경될 소지에 대비해야 하기 때문이다(즉, 할당될 수 있는 값의 범위가 넒어져야 한다)
  • 반면 const는 값이 변할 일이 없으므로 리터럴 타입으로 고정해버려도 크게 문제가 없는 것
  • 물론 만능은 아니며 객체나 배열에 대해서는 별 효과가 없다
interface Vector3D {
  x :number;
  y :number;
  z :number;
}
//keyof Vector3D = 'x' | 'y' | 'z'
function getComponent(vector :Vector3D, axis : keyof Vector3D) {
  return vector[axis];
}

let x로 선언하면 string으로 추론된다
반면 const x로 선언하면 'x' 리터럴 타입으로 추론된다
따라서 아래의 경우 const x로 선언하면 오류가 사라진다

let x = 'x';
let vec = { x: 10, y: 20, z: 30 };
getComponent(vec, x);
// Argument of type 'string' is not assignable to parameter of type 'keyof Vector3D'

 

2.2 명시적 타입 구문 제공

바로 위에서 const는 만능이 아니라고 설명했다. 그 이유는 객체의 경우 TS의 넓히기 알고리즘이 각 요소를 let으로 할당된 것 처럼 다루기 때문이다. 즉, const v = { x: 1 }; 이라고 const 키워드로 선언한들 x의 타입은 number로 추론된다.

v.x의 타입은 1이라는 리터럴 타입이길 바랬지만
객체의 경우 const로 선언한들 let처럼 동작한다

const v = {
  x: 1 // number 타입이 된다
}

여기에 명시적 타입을 제공하면
const v : { x: 1|3|5} = {
  x: 1 // x : 1|3|5 리터럴 타입들의 유니온 타입이 된다
       // number로 추론되진 않았따!
}

 

2.4 추가적인 문맥 제공

[3.26]에서 설명할 예정

 

2.5 const 단언문 (as const)

TS에게 최대한 좁은 타입으로 추론하게끔 강제한다. 객체와 더불어 배열에도 사용할 수 있다.

const v1 = {
  x: 1,  // number
  y: 2,  // number
}

x: 1에 한해 가장 좁은 타입으로 추론한다
const v2 = {
  x: 1 as const, // 1 (literal type)
  y: 2           // number
}

v3 객체를 가장 좁은 타입으로 추론한다
즉, 다른 타입들과 부분집합이 발생할 여지가 없게 좁힌다
그렇다면 가장 좁고 구체적인 타입은??
const v3 = {
  x: 1,
  y: 2,
} as const // { readonly x :1; readonly y :2;}

readonly로 선언되면
초기화 이후 재선언, 재할당이 불가능하므로
변경될 여지도 없고 타입이 변할 이유도 없는 
가장 구체적이면서 좁은 타입이된다