Typescript

[Typescript] 이펙티브 타입스크립트 3 아이템21-22

sian han 2023. 3. 13. 09:30

타입스크립트는 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 타입이다. 

 

 

 

요약

  • 분기문 외에도 여러 종류의 제어 흐름을 살펴보며 타입스크립트가 타입을 좁히는 과정을 이해해야 한다.
  • 태그된/구별된 유니온과 사용자 정의 타입가드를 사용해 타입좁히기 과정을 원활하게 만들 수 있다.