[Effective Typescript] 타입 추론(2) - 타입 넓히기, 타입 좁히기

이예슬·2022년 11월 20일
0

Effective TypeScript

목록 보기
8/15

Item21. 타입 넓히기

변수를 초기화할 때 타입을 명시하지 않으면 타입체커는 타입을 결정해야 한다. 이 때 타입스크립트는 지정된 단일 값을 가지고 할당 가능한 값들의 집합을 유추하는데 이 과정을 타입 넓히기라고 한다.

다음 예제는 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 는 어떤 타입으로 추론될 수 있을까?

  • [’x’, 1]
  • [string, number]
  • readonly[string, number]
  • (string | number)[] …

mixed 의 타입은 위에 적힌 타입 말고도 any 등 더 많은 타입으로 추론될 수 있다. 즉 정보가 충분하지 않을 경우 mixed 가 어떤 타입으로 추론되어야 할지 알 수 없다.

이러한 타입 넓히기 과정을 제어하기 위해 몇 가지 방법을 사용할 수 있다.

const 사용하기

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 단언문 사용하기

const 단언문은 타입 공간 기법 중 하나로 값 뒤에 as const를 작성하면 타입스크립트는 최대한 좁은 타입으로 추론하게 된다.

const v = {
	x: 1 as const 
	y: 2 
} 

위 코드처럼 const 단언문을 사용하게 되면 타입체커는 해당 객체의 타입을 {x: 1; y: number}와 같이 추론한다.

Item22. 타입 좁히기

타입 좁히기는 타입스크립트가 넓은 타입으로부터 좁은 타입으로 진행하는 과정을 말한다.

타입스크립트에서는 몇 가지 방법을 사용해서 타입 좁히기를 진행할 수 있다.

분기문 사용하기

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

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)

profile
꾸준히 열심히!

0개의 댓글