Item 26 타입 추론에 문맥이 어떻게 사용되는지 이해하기

About

타입스크립트는 타입을 추론할 때 단순히 값만 고려하는 것이 아니라 값이 존재하는 곳의 문맥까지도 살핀다.

그런데 문맥을 고려해 타입을 추론하면 가끔 이상한 결과가 나온다.

이때 타입 추론에 문맥이 어떻게 사용되는지 이해하고 있다면 제대로 대처할 수 있다.

Cautions with String Literals

자바스크립트는 코드의 동작과 실행 순서를 바꾸지 않으면서 표현식을 상수로 분리해 낼 수 있다.

// 인라인 형태
setLanguage('JavaScript')

// 참조 형태
let language = 'JavaScript'
setLanguage(language)

타입스크립트에서는 다음 리팩터링이 여전히 동작한다.

function setLanguage(language: string) { /* ... */ }

setLanguage('JavaScript') // OK

let language = 'JavaScript'
setLanguage(language)     // OK

이제 문자열 타입을 더 특정해 문자열 리터럴 타입의 유니온으로 바꾼다고 가정해보자. (Item 33에서 더 자세히 다룬다)

type Language = 'JavaScript' | 'TypeScript' | 'Python'
function setLanguage(language: Language) { /* ... */ }

setLanguage('JavaScript') // OK

let language = 'JavaScript'
setLanguage(language)
         // ~~~~~~~~ 'string' 형태의 인수는 'Language' 형식의 매개변수에 할당될 수 없습니다.

인라인(inline) 형태에서 타입스크립트는 함수 선언을 통해 매개변수가 Language 타입이어야 한다는 것을 알고 있다. 해당 타입에 문자열 리터럴 'JavaScript'는 할당 가능하므로 정상이다.

그러나 이 값을 변수로 분리해내면, 타입스크립트는 할당 시점에 타입을 추론한다. 이 경우엔 string으로 추론했고, Language 타입으로 할당이 불가능하므로 오류가 발생했다.

해결 방법은 두 가지이다.

  1. 타입 선언에서 language의 가능한 값을 제한하기

  2. language를 상수로 만들기

타입 선언에서 language의 가능한 값 제한하기

let language: Language = 'JavaScript'
setLanguage(language)
  • language 값에 오타가 있으면 오류를 표시해주는 장점이 있다.

language를 상수로 만들기

const language = 'JavaScript'
setLanguage(language)
  • 타입스크립트는 language에 대해서 더 정확한 타입인 문자열 리터럴 "JavaScript"로 추론할 수 있다.

  • language를 재할당해야 한다면 타입 선언이 필요하다. (Item 21 타입 넓히기)

그런데 이 과정에서 사용되는 문맥으로부터 값을 분리했다. 문맥과 값을 분리하면 추후에 근본적인 문제를 발생시킬 수 있다. 이러한 문맥의 소실로 인해 오류가 발생하는 몇 가지 경우와, 이를 어떻게 해결하는지 하나하나 살펴보자.

Cautions with Tuples

function panTo(where: [number, number]) { /* ... */ }

panTo([10, 20]) // OK

const loc = [10, 20]
panTo(loc)
   // ~~~ 'number[]' 형식의 인수는 '[number, number]' 형식의 매개변수에 할당될 수 없습니다.

첫 번째 경우는 [10, 20]이 튜플 타입 [number, number]에 할당 가능하다.

두 번째 경우는 타입스크립트가 loc의 타입을 number[]로 추론한다. (즉, 길이를 알 수 없는 숫자의 배열)

그러면 any를 사용하지 않고 오류를 고쳐보자.

1. Type Declaration

타입스크립트가 의도를 제대로 파악하도록 타입 선언을 제공해 해결할 수 있다.

const loc: [number, number] = [10, 20]
panTo(loc) // OK

2. Provide const Context

any를 사용하지 않고 오류를 고칠 수 있는 또 다른 방법은 '상수 문맥'을 제공하는 것이다. 단, as const만으로는 readonly가 붙어버리니 함수를 고쳐줘야 한다.

function panTo(where: readonly [number, number]) { /* ... */ }
const loc = [10, 20] as const
panTo(loc) // OK

as const는 문맥 손실과 관련된 문제를 깔끔하게 해결할 수 있지만, 단점이 있다.

만약 타입 정의에 실수가 있다면(e.g., 튜플에 세 번째 요소 추가) 오류는 타입 정의가 아니라 호출되는 곳에서 발생한다.

특히 여러 겹 중첩된 객체에서 오류가 발생한다면 근본적인 원인을 파악하기 어렵다.

Cautions with Objects

문맥에서 값을 분리하는 문제는 문자열 리터럴이나 튜플을 포함하는 큰 객체에서 상수를 뽑아낼 때도 발생한다.

type Language = 'JavaScript' | 'TypeScript' | 'Python'
interface GovernedLanguage {
  language: Language
  organization: string
}
function complain(language: GovernedLanguage) { /* ... */ }

complain({ language: 'JavaScript', organization: 'Microsoft' }) // OK

const ts = {
  language: 'TypeScript',
  organiztaion: 'Microsoft',
}

complain(ts)
      // ~~ { language: 'JavaScript', organization: 'Microsoft' } 형태의 인수는
      // 'GovernedLanguage' 형식의 매개변수에 할당될 수 없습니다.
      // 'language' 속성의 형식이 호환되지 않습니다.
      // 'string' 형식은 'Language' 형식에 할당할 수 없습니다.

이 문제는 타입 선언을 추가하거나 (const ts: GovernedLanguage = ...) 상수 단언(as const)을 사용해 해결한다. (Item 9 타입 단언보다는 타입 선언을 사용하기)

Cautions with Callbacks

콜백을 다른 함수로 전달할 때, 타입스크립트는 콜백의 매개변수 타입을 추론하기 위해 문맥을 사용한다.

function callWithRandomNumbers(fn: (n1: number, n2: number) => void) {
  fn(Math.random(), Math.random())
}

callWithRandomNumbers((a, b) => {
  a // 타입이 number
  b // 타입이 number
  console.log(a + b)
})

callWithRandomNumbers의 타입 선언으로 인해 a와 b의 타입이 number로 추론된다.

콜백을 상수로 뽑아내면 문맥이 소실되고 noImplicitAny 오류가 발생한다.

const fn = (a, b) => {
         // ~    'a' 매개변수에는 암시적으로 'any' 형식이 포함됩니다.
         //    ~ 'b' 매개변수에는 암시적으로 'any' 형식이 포함됩니다.
  console.log(a + b)
}
callWithRandomNumbers(fn)

이런 경우에는 매개변수에 타입 구문을 추가해 해결할 수 있다.

const fn = (a: number, b: number) => {
  console.log(a + b)
}
callWithRandomNumbers(fn)

또는 가능할 경우 전체 함수 표현식에 타입 선언을 적용하면 된다. (Item 12 함수 표현식에 타입 적용하기)

Summary

  • 타입 추론에서 문맥이 어떻게 쓰이는지 주의해서 살펴봐야 한다.

  • 변수를 뽑아서 별도로 선언했을 때 오류가 발생한다면 타입 선언을 추가해야 한다.

  • 변수가 정말로 상수라면 상수 단언(as const)를 사용해야 한다. 그러나 상수 단언을 사용하면 정의한 곳이 아니라 사용한 곳에서 오류가 발생하므로 디버깅에 있어 주의해야 한다.

Last updated