타입스크립트는 x 의 타입을 string 으로 추론한다.
let x = 'x'
x = 'a'
x = 'Four score and seven years ago...'
자바스크립트에서는 다음과 같이 작성해도 유효하다.
let x = 'x'
x = /x|y|z
x = ['x','y','z'];
타입스크립트는 x 의 타입을 string 으로 추론할 때 명확성과 유연성 사이의 균형을 유지하려고 한다.
일반적인 규칙은 변수가 선언된 후로는 타입이 바뀌지 않아야 하므로
string | RegExp
string | string[]
any
보다는
string 을 사용하는게 낫다.
▶ 타입스크립트에서 넓히기 과정을 제어할 수 있도록 제공하는 방법
▷ const 사용하기
- let 대신 const로 변수를 선언하면 더 좁은 타입이 된다.
interface Vector3 {
x: number
y: number
z: number
}
function getComponent(vector: Vector3, axis: 'x' | 'y' | 'z') {
return vector[axis]
}
const x = 'x' // type is "x"
let vec = { x: 10, y: 20, z: 30 }
getComponent(vec, x) // OK
const 로 선언된 x는 재할당이 될 수 없으므로 타입스크립트는 의심의 여지 없이 더 좁은 타입('x') 으로 추론할 수 있다.
그리고 문자열 리터럴 타입 "x"는 "x"|"y"|"z" 에 할당 가능하므로 코드가 타입 체커를 통과한다.
다음 코드는 자바스크립트에서 정상이다
const v = {
x: 1,
}
v.x = 3
v.x = '3'
v.y = 4
v.name = 'Pythagoras'
그렇지만 타입스크립트에서는 마지막 세 문장에서 오류가 발생한다.
const v = {
x: 1,
}
v.x = 3 // OK
v.x = '3'
// ~ Type '"3"' is not assignable to type 'number'
v.y = 4
// ~ Property 'y' does not exist on type '{ x: number; }'
v.name = 'Pythagoras'
// ~~~~ Property 'name' does not exist on type '{ x: number; }'
타입스크립트는 오류를 잡기 위해 충분히 구체적으로 타입을 추론해야하지만, 잘못된 추론을 할 정도로 구체적으로 수행하지는 않는다.
타입 추론의 강도를 직접 제어하려면 타입스크립트의 기본 동작을 재정의 해야 한다.
▶ 타입 스크립트의 기본동작을 제어하는 방법 (타입 추론의 강도를 제어하기 위함)
1. 명시적 타입 구문 제공
- x 에는 1,3,5 만 들어갈 수 있음을 명시
function getComponent(vector: Vector3, axis: 'x' | 'y' | 'z') {
return vector[axis]
}
const v: { x: 1 | 3 | 5 } = {
x: 1,
} // Type is { x: 1 | 3 | 5; }
2. 타입 체커에 추가적인 문맥을 제공
- ex ) 함수의 매개변수로 값을 전달
3. const 단언문 사용
- const 단언문 != let | const
- 혼동해서는 안된다. const 단언문은 온전히 타입 공간의 기법이다.
function getComponent(vector: Vector3, axis: 'x' | 'y' | 'z') {
return vector[axis]
}
const v1 = {
x: 1,
y: 2,
} // Type is { x: number; y: number; }
const v2 = {
x: 1 as const,
y: 2,
} // Type is { x: 1; y: number; }
const v3 = {
x: 1,
y: 2,
} as const // Type is { readonly x: 1; readonly y: 2; }
위와 같이 값 뒤에 as const 를 작성하면, 타입스크립트는 최대한 좁은 타입으로 추론한다.
요약
- 타입스크립트가 넓히기를 통해 상수의 타입을 추론하는 법을 이해해야 한다.
- 동작에 영향을 줄 수 있는 방법인 const, 타입구문, 문맥, as const 에 익숙해져야 한다.
※ 타입 좁히기
타입 좁히기는 타입스트립트가 넓은 타입으로부터 좁은 타입으로 진행하는 과정을 말한다.
▶ 타입좁히기 방법
▷ null 체크
- 가장 일반적인 타입 좁히기 예시
const el = document.getElementById('foo') // Type is HTMLElement | null
if (el) {
el // Type is HTMLElement
el.innerHTML = 'Party Time'.blink()
} else {
el // Type is null
alert('No element #foo')
}
만약 el 이 null 이라면 첫번째 블록이 실행되지 않는다. 첫번째 블록에서 HTMLElement | null 타입의 null 을 제외하므로, 더 좁은 타입이 되어서 작업이 훨씬 쉬워지는 것이다.
▷ 분기문에서 예외를 던지거나 함수를 반환하여 블록의 나머지 부분에서 변수의 타입을 좁힌다.
const el = document.getElementById('foo') // Type is HTMLElement | null
if (!el) throw new Error('Unable to find #foo')
el // Now type is HTMLElement
el.innerHTML = 'Party Time'.blink()
▷ 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
}
▷ Array.isArray 같은 내장함수로 타입을 좁힌다
function contains(text: string, terms: string | string[]) {
const termList = Array.isArray(terms) ? terms : [terms]
termList // Type is string[]
// ...
}
terms 가 배열이면 termList 는 terms 이고,
terms 가 배열이아니면 terms 를 배열로 만들어서 termList 에 할당해라.
따라서 termList는 무조건 배열이다.
▷ 명시적 태그를 붙여 타입을 좁힌다
태그된 유니온 (tagged unione) 또는 구별된 유니온(discriminated union) 이라고 불린다.
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 // Type is DownloadEvent
break
case 'upload':
e // Type is UploadEvent
break
}
}
만약 타입스크립트가 이를 식별하지 못하면 커스텀 함수를 도입할 수 있다.
이를 사용자 정의 타입 가드 라고 한다.
function isInputElement(el: HTMLElement): el is HTMLInputElement {
return 'value' in el
}
function getElementContent(el: HTMLElement) {
if (isInputElement(el)) {
el // Type is HTMLInputElement
return el.value
}
el // Type is HTMLElement
return el.textContent
}
첫번째 isInputElement 함수는 반환타입이 el is HTMLInputElement 이다.
함수의 반환이 true 인 경우, 타입 체커에 매개변수의 타입을 좁힐 수 있다고 알려준다.
isInputElement 함수는 타입체커에게 알려주기 위한 용도이다.
▷ 함수에 타입 가드를 사용해 타입을 좁힐 수 있다.
- 배열과 객체의 타입 좁히기가 가능하다
- 탐색할 때 undefined 가 될 수 있는 타입을 사용할 수 있다.
const jackson5 = ['Jackie', 'Tito', 'Jermaine', 'Marlon', 'Michael']
const members = ['Janet', 'Michael'].map(who => jackson5.find(n => n === who)) // Type is (string | undefined)[]
이때 filter 함수를 사용해 undefined 를 걸러내보자
const jackson5 = ['Jackie', 'Tito', 'Jermaine', 'Marlon', 'Michael']
const members = ['Janet', 'Michael'].map(who => jackson5.find(n => n === who))
.filter(who => who !== undefined) // Type is (string | undefined)[]
filter 함수로 undefined 를 걸러내지지 않았다. 이럴때 타입가드를 사용하면 타입을 좁힐 수 있다 .
const jackson5 = ['Jackie', 'Tito', 'Jermaine', 'Marlon', 'Michael']
function isDefined<T>(x: T | undefined): x is T {
return x !== undefined
}
const members = ['Janet', 'Michael'].map(who => jackson5.find(n => n === who))
.filter(isDefined) // Type is string[]
isDefined<T> 의 반환값은 boolean 이다.
함수 내부에서 undefined 가 아닐 때 리턴을 하게끔 만들어서 true 라면 'x는 T 에 속한다' 가 되어
undefined 를 걸러내는용도로 isDefined 함수가 사용된다.
▶ 타입스크립트에서 타입을 좁히기 위한 잘못된 방법
1. 유니온 타입에서 null 을 제외하기 위해 잘못된 방법을 사용함
const el = document.getElementById('foo') // type is HTMLElement | null
if (typeof el === 'object') {
el // Type is HTMLElement | null
}
null 도 typeof 를 하면 object 가 나온다. 따라서 잘못된 방법 !
2. 기본형 값이 잘못됨
function foo(x?: number | string | null) {
if (!x) { // undefined / null / 0 / ''
x // Type is string | number | null | undefined
}
}
조건문을 달아서 타입을 좁힐려고 했지만 undefined / null / 0 / '' 는 falsy 한 값이기 때문에
x 는 여전히 string | number | null | undefined 타입이다.
요약
- 분기문 외에도 여러 종류의 제어 흐름을 살펴보며 타입스크립트가 타입을 좁히는 과정을 이해해야 한다.
- 태그된/구별된 유니온과 사용자 정의 타입가드를 사용해 타입좁히기 과정을 원활하게 만들 수 있다.
'Typescript' 카테고리의 다른 글
[Typescript] 이펙티브 타입스크립트 4 아이템28 (0) | 2023.03.16 |
---|---|
[Typescript] 이펙티브 타입스크립트 3 아이템23-25 (0) | 2023.03.15 |
[Typescript] 이펙티브 타입스크립트 3 아이템19-20 (0) | 2023.03.09 |
[Typescript] 이펙티브 타입스크립트 2장 아이템17-18 (0) | 2023.03.07 |
[Typescript] 이펙티브 타입스크립트 2장 아이템15-16 (0) | 2023.03.07 |