책 정리/이펙티브 타입스크립트

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

Rulu_ 2023. 7. 26. 14:34
  • 타입스크립트는 타입을 추론할 때 단순히 값만 고려하지 않음
    • 값이 존재하는 곳의 문맥까지 고려
    • 문맥을 고려해 타입을 추론할 때 이상한 결과가 나올 수도
  • 자바스크립트와 타입스크립트의 차이
    • 자바스크립트
    • function setLanguage(language: string) { /* ... */ } setLanguage('JavaScript'); // OK let language = 'JavaScript'; setLanguage(language); // OK
    • 타입스크립트
      • inline 형태에서 TS는 함수 선언을 통해 매개변수가 Language타입이어야 함을 앎
      • 값을 변수로 분리하면, TS는 할당 시점에 타입을 추론
    • type Language = 'JavaScript' | 'TypeScript' | 'Python'; function setLanguage(language: Language) { /* ... */ } setLanguage('JavaScript'); // OK let language = 'JavaScript'; setLanguage(language); // ~~~~~~~~ Argument of type '**string**' is not assignable // to parameter of type 'Language'
    • 문제 해결 방법
      1. 타입 선언에서 language의 가능한 값을 제한 하는 것
      2. let language: Language = 'JavaScript';
      3. language를 상수로 만드는 것
        • const를 사용하여 타입 체커에게 language는 변경할 수 없음을 알려줌
      4. const language = 'JavaScript';
      → 사용되는 문맥으로부터 값을 분리함. 문맥과 값을 분리하면 문제 발생할 수 있음

문맥의 소실로 인해 발생하는 오류

튜플 사용 시

type Language = 'JavaScript' | 'TypeScript' | 'Python';
function setLanguage(language: Language) { /* ... */ }
// Parameter is a (latitude, longitude) pair.
function panTo(where: [number, number]) { /* ... */ }

panTo([10, 20]);  // OK

const loc = [10, 20];
panTo(loc);
//    ~~~ Argument of type 'number[]' is not assignable to
//        parameter of type '[number, number]'
  • 문맥과 값을 분리할 경우, number[]로 추론하기 때문에 많은 배열이 이와 맞지 않는 수의 요소를 가지므로 튜플 타입에 할당할 수 없음

문제 해결 방법

  1. 타입 선언을 제공하는 방법
  2. const loc: [number, number] = [10, 20]; panTo(loc); // OK
  3. ‘상수 문맥’을 제공하는 방법
    • const는 단지 값이 가리키는 참조가 변하지 않는 얕은 상수
    • as const는 그 값이 내부까지 상수
      • 단, 이 추론은 ‘과하게’ 정확함
    • const loc = [10, 20] as const; panTo(loc); // ~~~ Type 'readonly [10, 20]' is 'readonly' // and cannot be assigned to the mutable type '[number, number]'
    • ponTo함수에도 readonly 추가
    • function panTo(where: readonly [number, number]) { /* ... */ } const loc = [10, 20] as const; panTo(loc); // OK
    • as const는 문맥 손실과 관련한 문제를 해결할 수 있지만, 타입 정의에 실수가 있다면 오류는 타입 정의가 아닌 호출되는 곳에서 발생
      • 여러 겹 중첩된 객체에서 오류가 발생한다면 원인 파악의 어려움
      function panTo(where: readonly [number, number]) { /* ... */ }
      const loc = [10, 20, 30] as const;  // error is really here.
      panTo(loc);
      //    ~~~ Argument of type 'readonly [10, 20, 30]' is not assignable to
      //        parameter of type 'readonly [number, number]'
      //          Types of property 'length' are incompatible
      //            Type '3' is not assignable to type '2'
      
    ⇒ 타입 시그니처를 수정할 수 없는 경우 : 타입 구문 사용

객체 사용 시

type Language = 'JavaScript' | 'TypeScript' | 'Python';
interface GovernedLanguage {
  language: Language;
  organization: string;
}

function complain(language: GovernedLanguage) { /* ... */ }

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

const ts = {
  language: 'TypeScript',
  organization: 'Microsoft',
};
complain(ts);
//       ~~ Argument of type '{ language: string; organization: string; }'
//            is not assignable to parameter of type 'GovernedLanguage'
//          Types of property 'language' are incompatible
//            Type 'string' is not assignable to type 'Language'

문제 해결 방법

  1. 타입 선언을 추가 (const ts: GovernedLanguage = …)
  2. 상수 단언(as const)사용

콜백 사용 시

  • 콜백을 다른 함수로 전달 시 TS는 콜백의 매개변수 타입을 추론하기 위해 문맥 사용
function callWithRandomNumbers(fn: (n1: number, n2: number) => void) {
  fn(Math.random(), Math.random());
}

callWithRandomNumbers((a, b) => {
  a;  // Type is number
  b;  // Type is number
  console.log(a + b);
});
  • 콜백을 상수로 뽑아내면 문맥이 소실되고 noImplicitAny 오류 발생
const fn = (a, b) => {
         // ~    Parameter 'a' implicitly has an 'any' type
         //    ~ Parameter 'b' implicitly has an 'any' type
  console.log(a + b);
}
callWithRandomNumbers(fn);

문제 해결 방법

  1. 매개변수에 타입 구문을 추가
  2. const fn = (a: number, b: number) => { console.log(a + b); } callWithRandomNumbers(fn);
  3. 전체 함수 표현식에 타입을 적용
  4. type Tfn = (a: number, b: number) => void; const fn: Tfn = (a, b) => { console.log(a + b); }

요약

  • 타입 추론에서 문맥이 어떻게 쓰이는지 주의해야 함
  • 변수를 뽑아서 별도로 선언했을 때 오류가 발생한다면 타입 선언을 추가해야 함
  • 변수가 정말로 상수라만 상수 단언(as const)사용해야함
    • 상수 단언을 사용하면 정의한 곳이 아니라 사용한 곳에서 오류가 발생하므로 주의해야 함