이펙티브 타입스크립트 - 3장

Sohee Park·2023년 2월 16일
0
post-thumbnail

타입 추론에서 발생할 수 있는 몇가지 문제와 그 해법을 안내한다.

ITEM 19 추론 가능한 타입을 사용해 장황한 코드 방지하기

타입스크립트가 타입을 추론할 수 있다면 타입구문을 작성하지 않는게 좋다.
(예를 들면 const a:number = 10와 같이 이미 10이란 숫자를 통해 타입을 알 수 있는데, 굳이 number를 명시할 필요는 없다.)

어떤 언어들은 매개변수의 최종 사용처까지 참고하여 타입을 추론하지만, 타입스크립트는 최종 사용처까지 고려하지 않았다. 타입스크립트에서 변수의 타입은 일반적으로 처음 등장할 때 결정된다.
비구조화 할당문은 모든 지역 변수의 타입이 추론되도록 한다.

의도된 반환 타입을 명시하면 얻을 수 있는 장점

  • 정확한 위치에 오류가 표시된다.
  • 함수에 대해 더욱 명확하게 알수 있다
    • 구현하기 전에 입력타입과 출력 타입이 무엇인지 알아야 한다. 이런 방법은 함수를 구현하기 전에 테스트를 먼저 작성하는 테스트 주도 개발과 비슷하다.
  • 명명된 타입을 사용하여 직관적인 표현을 할 수 있다.
  • 타입에 대한 주석을 작성할 수 있다.

린터를 사용하고 있다면 eslint 규칙 중 no-inferrable-types을 사용하여 작성된 모든 타입 구문이 정말로 필요한지 확인할 수 있다.

ITEM 20 다른 타입에는 다른 변수 사용하기

다른 타입과 변수를 사용하는게 바람직한 이유

  • 서로 관련이 없는 두개의 값을 분리한다.
  • 변수명을 더 구체적으로 지을 수 있다.
  • 타입 추론을 향상시키며, 타입 구문이 불필요해진다.
  • 타입이 좀 더 간결해진다
  • let대신 const로 변수를 선언하게 된다. const로 변수를 선언하면 코드가 간결해지고 타입체커가 타입을 추론하기에도 좋다.

가려지는(shadowed) 변수를 혼동해서는 안된다.

function fetchProduct(id: string) {}
function fetchProductBySerialNumber(id: number) {}
const id = "12-34-56";
fetchProduct(id);

{
  const id = 123456;  // OK
  fetchProductBySerialNumber(id);  // OK
}

동일한 변수명에 타입이 다르다면 타입스크립트 코드는 잘 동작할지 몰라도 사람에게 혼란을 줄 수 있다. 변수의 값은 바뀔 수 있지만 타입은 일반적으로 바뀌지 않는다는 사실을 기억한다.

ITEM 21 타입 넓히기

상수를 사용해서 변수를 초기화 할 때 타입을 명시하지 않으면 타입체커는 타입을 결정해야 한다. 이 말은 지정된 단일 값을 가지고 할당 가능한 값들의 집합을 유추해야 한다는 뜻이다. 이 과정을 타입스크립트에서는 넓히기(widening)라고 부른다.

타입 넓히기가 진행될 때, 주어진 값으로 추론 가능한 타입이 여러개가 되면서 과정이 상당이 모호하고, 명확성과 유연성 사이의 균형을 유지하려고 한다.

넓히기 과정을 제어하는 방법

  • const를 사용하기
  • 명시적인 타입 구문을 제공하기
  • 타입 체커에 추가적인 문맥을 제공하기(함수의 매개변수로 값을 전달)
  • const 단언문(as const)을 사용하기

ITEM 22 타입 좁히기

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
}

일부 내장 함수로 타입을 좁히기

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

명시적 '태그'를 붙이기

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

이 패턴은 태그된 유니온(tagged union) 또는 구별된 유니온(discriminated union)이라고 불리며, 타입스크립트 어디에서나 찾아볼 수 있다.

사용자 정의 타입 가드를 만들기

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

ITEM 23 한꺼번에 객체 생성하기

  • 속성을 제각각 추가하지말고 한꺼번에 객체로 만든다.
  • 안전한 타입으로 속성을 추가하려면 객체 전개({...a, ...b})를 사용하면 된다.
  • 객체에 조건부로 속성을 추가하는 방법을 사용한다.
declare let hasMiddle: boolean;
const firstLast = {first: 'Harry', last: 'Truman'};
function addOptional<T extends object, U extends object>(
  a: T, b: U | null
): T & Partial<U> {
  return {...a, ...b};
}

const president = addOptional(firstLast, hasMiddle ? {middle: 'S'} : null);
president.middle  // OK, type is string | undefined

ITEM 24 일관성 있는 별칭 사용하기

  • 별칭은 타입스크립트가 타입을 좁히는 것을 방해한다. 따라서, 변수에 별칭을 사용할 때는 일관되게 사용한다.
  • 비구조화 문법을 사용해서 일관된 이름을 사용하는 것이 좋다.(타입도 따라감)
  • 함수 호출이 객체 속성의 타입 정제를 무효화할 수 있는 점을 주의한다.
    • 속성보다 지역변수를 사용하면 타입 정제를 믿을 수 있다.

ITEM 25 비동기 코드에는 콜백 대신 async함수 사용하기

콜백보다는 프로미스를 사용하는게 코드 작성과 타입 추론면에서 유리하다.
async함수에서 프로미스를 반환하면 또 다른 프로미스로 래핑되지 않는다. 반환타입은 Promise<Promise<T>>가 아닌 Promise<T>가 된다.

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

변수를 뽑아서 별도로 선언했을 때 오류가 발생한다면 타입 선언을 추가해야 한다.
변수가 정말로 상수라면 상수 단언(as const)를 사용해야 한다. 그러나 상수 단언을 사용하면 정의한 곳이 아니라 사용한 곳에서 오류가 발생하므로 주의해야 한다.

ITEM 27 함수형 기법과 라이브러리로 타입 흐름 유지하기

내장된 함수형 기법들과 로대시 같은 라이브러리에 타입 정보가 잘 유지되는 이유는 함수 호출 시 전달된 매개변수 값을 건드리지 않고 매번 새로운 값을 반환함으로써 새로운 타입으로 안전하게 반환할 수 있다.

타입 흐름을 개선하고, 가독성을 높이고, 명시적인 타입 구문의 필요성을 줄이기 위해 직접 구현하기보다는 내장된 함수형 기범과 로대시 같은 유틸리티 라이브러리를 사용하는 것도 좋다.

profile
고양이 두마리를 모시고 있는 프론트엔드 코더(?)

0개의 댓글