반응형
250x250
Notice
Recent Posts
Recent Comments
Link
관리 메뉴

짧은코딩

타입 넓히기와 좁히기 본문

TS/이펙티브 타입스크립트

타입 넓히기와 좁히기

5_hyun 2023. 3. 5. 17:42

타입 넓히기

런타임 시 모든 변수는 유일한 값을 가진다. 하지만 타입스크립에서는 코드를 정적 분석하는 시점에서 변수는 가능한 값들의 집합인 타입을 가진다.타입을 명시하지 않으면 타입 체커는 할당 가능한 값들의 집합을 유추한다. 이러한 과정을 타입 넓히기(widening)이라고 부른다. 

타입 넓히기의 문제점

-실행은 되지만 에디터에 오류가 나는 코드

interface Vector3 {
  x: number
  y: number
  z: number
}
function getComponent(vector: Vector3, axis: 'x' | 'y' | 'z') {
  return vector[axis]
}
let x = 'x'
let vec = { x: 10, y: 20, z: 30 }
getComponent(vec, x) // 이 부분에서 x를 string으로 인식해서 에러가 나게된다.

매개변수 axis에서는 타입을 'x' | 'y' | 'z'로 할당했는데 인자에 x는 string 타입 추론되어서 에러가 난다.

 

-타입 추론의 모호성

const mixed = ['x', 1];

이렇게 타입을 명시하지 않는 변수가 있다고 하면 타입이 될 수 있는 집합이 엄청 많아진다.

이렇게 많은 후보들이 있다.

에디터는 위 사진처럼 타입을 추론하게된다. 

타입스크립트가 타입 추론을 할 수 있지만 정확한 타입을 추론할 수 없는 것이 문제이다.

타입 넓히기 제어 방법

-const 사용하기

const x = "x";

let보다 const를 사용하는 것이 더 좁은 타입을 추론하게 된다. 왜냐하면 const로 변수를 선언하면 재할당 될 수 없어서 위 코드에서는 타입을 "x"로 생각한다. 하지만 const도 객체나 배열에서는 문제가 발생한다.

 

-배열이나 객체

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; }'

이 코드는 js에서는 정상이다. v의 타입은 구체적으로 {readonly x: 1}이다. 추상적으로는 {x: number}이다. 더 추상적으로는 {[key: string]: number} 혹은 object이다.

객체는 타입 넓히기 과정에서 각 요소를 let으로 할당된 것 처럼 다룬다. 그래서 v의 타입은 {x: number}이다. v.x를 다른 number로 재할당은 가능하다. 그리고 다른 속성을 추가하지 못한다. 따라서 주석을 제외한 아래 3줄에 오류가 발생한다. 이는 객체를 한번에 만들어서 해결할 수 있다.

타입 추론 강도 제어하기

1. 명시적 타입 구문 제공

const v: { x: 1 | 3 | 5 } = {
  x: 1,
} // Type is { x: 1 | 3 | 5; }

이런식으로 명시적으로 타입을 적어주면 된다.

 

2. 타입 체커에 추가적인 문맥을 제공하기

함수의 매개변수로 값을 전달하면 가능하다.

 

3. const 단언문 사용하기

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를 사용하면 최대한 좁은 타입으로 추론한다. 배열을 튜플 타입으로 추론할 때도 as const를 사용할 수 있다.

const a1 = [1, 2, 3] // Type is number[]
const a2 = [1, 2, 3] as const // Type is readonly [1, 2, 3]

또 as const를 붙이면 readonly가 붙는 것을 볼 수 있다.


타입 좁히기

타입 넓히기의 반대는 타입 좁히기이다. 타입 좁히기는 넓은 타입부터 좁은 타입으로 진행하는 과정을 말한다.

타입 좁히기 좋은 예시

-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이면 첫번째 분기문에서 블록이 실행되지 않아 더 좁은 타입이 되어 작업이 쉬워진다. 타입 체커는 일반적으로 조건문에서 타입 좁히기를 한다. 하지만 타입 별칭이 존재하면 그러지 못할 수도 있다.

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()

혹은 이렇게 예외 처리를 하고 타입을 좁힐 수 있다.

 

-Array.isArray로 좁히기

function contains(text: string, terms: string | string[]) {
  const termList = Array.isArray(terms) ? terms : [terms]
  termList // Type is string[]
  // ...
}

이런식으로도 타입을 좁힐 수 있다.

 

=> 이런식으로 타입스크립트는 조건문에서 타입을 좁히는 것에 능숙하다. 하지만 타입 좁히기를 잘못할 수 있는 가능성이 있어서 꼼꼼히 봐야한다.

타입 좁히기 잘못된 예시

-유니온 타입에서 null 제외

const el = document.getElementById('foo') // type is HTMLElement | null
if (typeof el === 'object') {
  el // Type is HTMLElement | null
}

typeof null이 "object"가 되기 때문에 if 구문에서 null이 제외되지 않았다.

 

-기본값이 잘못된 사례

function foo(x?: number | string | null) {
  if (!x) {
    x // Type is string | number | null | undefined
  }
}

이 경우에 x가 ""이나 0이면 !x는 true가 되어 if문이 실행된다.

타입 좁히기를 잘 할 수 있는 방법

1. 명시적 태그 붙이기

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
  }
}

 

이렇게 태그된 유니온을 사용하면 더 쉽게 타입을 좁힐 수 있다.

 

2. 사용자 정의 타입 가드

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
}

반환 타입의 el is HTMLInputElement는 함수 반환이 true인 경우 매개변수의 타입을 좁힐 수 있다.

 

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[]

혹은 이렇게 타입 가드를 사용해서 undefined를 걸러낼 수 있다.

728x90
반응형
Comments