[기술서적 리뷰] 이펙티브 타입스크립트 - 3. 타입 추론 (아이템 19 ~ 23)

5 분 소요

DAN VANDERKAM님의 이펙티브 타입스크립트을 읽고 요악하는 포스트입니다.

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

let x: number = 12; // Bad
let x = 12; // Good

// Bad
const person: {
  name: string;
  born: {
    where: string;
    when: string;
  };
  // ...
} = {
  name: 'yhan',
  born: {
    where: 'Busan, South Korea',
    when: '25, 12, 1993',
  },
  // ...
};

// Good
const person = {
  name: 'yhan',
  born: {
    where: 'Busan, South Korea',
    when: '25, 12, 1993',
  },
  // ...
};
function square(nums: number[]) {
  return nums.map((x) => x * x);
}
const squares = square([1, 2, 3, 4]); // number[]

IDE가 추론가능한 타입에 대해서는 명시적 타입 구문이 필요하지 않습니다.

함수 및 메서드 시그니처에는 타입구문을 포함하고, 함수 내에서 생성된 지역변수에는 타입구문을 넣치 않는 것이 좋습니다. 타입구문을 생략하여 불필요한 코드들을 줄이고 로직에 집중할 수 있도록 하는 것이 좋습니다.

// Good
const elmo: Product = {
  name: 'Tickle Me Elmo',
  id: '048188 627152',
  price: 28.99,
};

객체 리터럴을 정의할 때는 타입을 명시해주면 오타 등의 오류를 잡는데 유용합니다 (잉여 속성 체크). 사용하는 곳이아닌, 선언하는 곳에서 오류를 발견할 수 있습니다.

interface Vector2D {
  x: number;
  y: number;
}
function add(a: Vector2D, b: Vector2D): Vector2D {
  return { x: a.x + b.x, y: a.y + b.y };
}

함수를 작성할 때도 반환타입을 구체적으로 명시함으로써 사용자가 타입명세를 확인할 때 혼동하지 않게 해주는 것이 좋습니다.

eslint의 no-inferrable-types 규칙을 사용하면 작성도니 타입구문이 정말로 필요한지 확인할 수 있습니다.

아이템 20. 다른 타입에는 다른 변수 사용하기

변수의 값은 바뀔 수 있지만 그 타입은 보통 바뀌지 않는다.

Bad

let id: string | number = '12-34-56';
fetchProduct(id);

id = 123456;
fetchProductBySerialNumber(id);

Good

const id = '12-34-56';
fetchProduct(id);

const serial = 123456;
fetchProductBySerialNumber(serial);

다른 타입에는 별도의 변수를 사용하는 것이 좋습니다.

  • 서로관련없는 두 값을 분리합니다.
  • 변수명을 구체적으로 지을 수 있습니다.
  • 타입 추론을 향상시키고, 타입 구문이 불필요해집니다
  • let 대신 const를 사용할 수 있습니다.

아이템 21. 타입 넓히기

타입스크립트가 작성된 코드를 체크하는 정적 분석 시점에, 변수는 가능한 값들의 집합을 가집니다.

타입명시 없이 변수를 초기화할 때 타입 체커는 지정된 값을 가지고 할당가능한 값들의 집합을 유추합니다. 이러한 과정을 넓히기(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); // 오류
Argument of type ‘string’ is not assignable to parameter of type ‘“x” “y” “z”’.

런타임에서는 문제 없지만 타입체커는 오류를 발생시킵니다.

변수 x는 할당시기에 string으로 추론되어 "x" | "y" | "z" 타입에 할당이 불가능 합니다.

x에 RegExp, Array, number등 다양한 값을 할당할 수 있지만 타입스크립트는 string으로 추론했습니다.

const mixed = ['x', 1]; // (string|number)[]

/**
 * ('x' | 1)[]
 * ['x', 1]
 * [string, number]
 * (string|number)[]
 * [any, any]
 * ...
 * /

추론할 수 있는 후보가 상당히 많지만, 타입 스크립트는 명확성과 유연성 사이에서 균형을 유지하려고 합니다.

이러한 타입스크립트의 넓히기 방법을 제어할 수 있는 몇가지 방법이 있습니다.

  • let 대신 const를 사용하면 더 좁은 타입이 됩니다.
const x = 'x';
let vec = { x: 10, y: 20, z: 30 };
getComponent(vec, x); // 성공
  • 객체는 한번에 만드는 것이 좋습니다.
const v = {
  x: 1,
};
v.x = 3; // 성공
v.x = '3'; // 오류
v.y = 4; // 오류
v.name = 'Pythagoras'; // 오류
  • 명시적 타입구문 사용
const v: { x: 1 | 3 | 5 } = {
  x: 1,
};
  • 추가적인 문맥 제공 (아이템 26)

  • const 단언문 사용

const v1 = {
  x: 1,
  y: 2,
}; // { x: number; y: number; }

const v2 = {
  x: 1 as const,
  y: 2,
}; // { x: 1; y: number; }

const v3 = {
  x: 1,
  y: 2,
} as const; // { readonly x: 1; readonly y: 2; }

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

as const 를 사용하면 타입스크립트는 최대한 좁은 타입으로 추론합니다.

아이템 22. 타입 좁히기

null 체크

const el = document.getElementById('foo'); // HTMLElement | null
if (el) {
  el; // HTMLElement
  el.innerHTML = 'Party Time'.blink();
} else {
  el; // null
  alert('No element #foo');
}

instanceof

function contains(text: string, search: string | RegExp) {
  if (search instanceof RegExp) {
    search; // RegExp
    return !!search.exec(text);
  }
  search; // string
  return text.includes(search);
}

typeof

null 값 조심하기

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

in

interface A {
  a: number;
}
interface B {
  b: number;
}
function pickAB(ab: A | B) {
  if ('a' in ab) {
    ab; // A
  } else {
    ab; // B
  }
}

내장함수

function contains(text: string, terms: string | string[]) {
  const termList = Array.isArray(terms) ? terms : [terms];
  termList; // 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; // DownloadEvent
      break;
    case 'upload':
      e; // UploadEvent
      break;
  }
}

커스텀 타입 가드

function isInputElement(el: HTMLElement): el is HTMLInputElement {
  return 'value' in el;
}

function getElementContent(el: HTMLElement) {
  if (isInputElement(el)) {
    el; // HTMLInputElement
    return el.value;
  }
  el; // HTMLElement
  return el.textContent;
}

아이템 23. 한꺼번에 객체 생성하기

객체를 생성할때는 여러 속성을 포함해 한꺼번에 생성해야 타입 추론에 유리합니다.

// Bad
const pt = {};
pt.x = 3; // 오류 ~ Property 'x' does not exist on type '{}'
pt.y = 4; // 오류 ~ Property 'y' does not exist on type '{}'

// Good
const pt = {
  x: 3,
  y: 4,
};
interface Point {
  x: number;
  y: number;
}
const pt = { x: 3, y: 4 };
const id = { name: 'Pythagoras' };

// Bad
const namedPoint = {};
Object.assign(namedPoint, pt, id);
namedPoint.name; // 오류 ~ Property 'name' does not exist on type '{}'

// Good
const namedPoint = { ...pt, ...id }; // {name: string; x: number; y: number;}

spread 연산자를 사용하면 객체를 한꺼번에 생성하며 타입 추론도 정확히 할 수 있습니다.

declare let hasMiddle: boolean;
const firstLast = { first: 'Harry', last: 'Truman' };
// {middle?: string; first: string; last: string;}
const president = { ...firstLast, ...(hasMiddle ? { middle: 'S' } : {}) }; 
const president2 = { ...firstLast, ...(hasMiddle && { middle: 'S' }) }; 

spread 연산자로 optional한 속성 추가도 할 수 있습니다.

// {first: string; last: string;} | {middle: string; gender: string; first: string; last: string;}
const president3 = { ...firstLast, ...(hasMiddle && { middle: 'S', gender: 'M'}) }; 
president3.middle // 오류

하지만 한번에 여러 속성을 optional하게 추가하면 유니온 타입이 만들어집니다. middle과 gender는 항상 함께 정의되기 때문입니다.

function addOptional<T extends object, U extends object>(a: T, b: U | null): T & Partial<U> {
  return {...a, ...b};
}

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

유니온 대신 optional 필드를 사용하고 싶다면 이런 헬퍼 함수를 사용할 수도 있습니다.