[2장] 타입스크립트의 타입 시스템 (2 / 2)
이펙티브 타입스크립트 2장의 아이템 12 - 18을 정리한 내용입니다.
작성일 2024.02.14
페이지가 생성된 시간 2024.11.16 14:27:45

0

아이템12 함수 표현식에 타입 적용하기

  • 타입스크립트에서는 함수 표현식을 사용하는게 더 좋음.
  • 함수 타입을 선언해 재사용할 수 있기 때문이다
  • 동일한 타입의 시그니처를 가지는 여러 개의 함수를 작성할 때
function add(a: number, b: number) {
  return a + b;
}
function sub(a: number, b: number) {
  return a - b;
}
function mul(a: number, b: number) {
  return a * b;
}
function div(a: number, b: number) {
  return a / b;
}
type BinaryFn = (a: number, b: number) => number;
const add: BinaryFn = (a, b) => a + b;
const sub: BinaryFn = (a, b) => a - b;
const mul: BinaryFn = (a, b) => a * b;
const div: BinaryFn = (a, b) => a / b;
  • 다른 함수의 시그니처와 동일한 타입을 가지는 새 함수를 작성할 때
  • 다른 함수의 시그니처를 참조하려면 typeof fn
const checkedFetch: typeof fetch = async (input, init) => {
  const response = await fetch(input, init);
  if (!response.ok) {
    throw new Error('Request failed: ' + response.status);
  }
  return response;
};

아이템13 타입과 인터페이스의 차이점

  • 명명된 타입 정의 시 타입과 인터페이스 둘 다 사용이 가능
type TState = {
  name: string;
  capital: string;
};
interface IState {
  name: string;
  capital: string;
}
  • 대부분의 경우에 둘 다 사용이 가능하지만 차이를 분명하게 알고 일관성있게 사용해야 함.
  • 함수타입에 추가적인 속성이 있다면 타입별칭과 인터페이스는 별 차이 없음
// 일반적인 함수타입 선언
type TFn = (x: number) => string;
interface IFn {
  (x: number): string;
}

// 추가적인 속성이 있을 때
type TFnWithProperties = {
  (x: number): number;
  prop: string;
};
interface IFnWithProperties {
  (x: number): number;
  prop: string;
}
  • 인터페이스는 유니온, 인터섹션을 통한 확장이 안됨(extends 는 가능)
// 확장 예시
// 인터페이스는 extends 사용
interface IStateWithPop extends TState {
  population: number;
}
// 타입은 인터섹션 사용
type TStateWithPop = IState & { population: number };
  • implements 할 때는 타입별칭, 인터페이스 모두 가능
class StateT implements TState {
  name: string = '';
  capital: string = '';
}
class StateI implements IState {
  name: string = '';
  capital: string = '';
}
  • 타입별칭은 매핑된 타입(xx in keyof XX), 조건부타입(삼항연산자) 가능
    • 매핑된 타입은 다음 아이템(14)에서 다룸
T extends U ? : X : Y
  • 타입별칭은 튜플과 배열타입 표현도 쉽다.
type A = [string, ...number[]];
const a: A = ['1', 1, 2, 3, 4, 5];
  • 인터페이스는 보강이 가능하다. 같은 이름으로 선언하면 합쳐짐. (보강 == 선언병합 ??)
interface IState {
  name: string;
  capital: string;
}
interface IState {
  population: number;
}
const wyoming: IState = {
  name: 'Wyoming',
  capital: 'Cheyenne',
  population: 500_000,
};

결론: 추가적인 보강이 필요할 것 같다면 인터페이스를 쓰자. 그 외에는 타입.. 이것이 정답은 아니지만 중요한 것은 같은 상황에서는 동일한 방법을 써서 일관성을 유지하는 것이 중요

아이템14 타입 연산과 제네릭 사용으로 반복 줄이기

  • 타입간 매핑하는 방법을 알아야 타입 정의에서도 DRY의 장점을 적용 가능하다
  • 타입을 확장하는 방법
    • 인터페이스 extends
    • 유니온, 인터섹션 타입
  • 타입의 부분만 표현하는 방법: 매핑된 타입
interface State {
  userId: string;
  pageTitle: string;
  recentFiles: string[];
  pageContents: string;
}

type TopNavState = {
  [k in 'userId' | 'pageTitle' | 'recentFiles']: State[k];
};
type TopNavState = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;
  • 태그된 유니온에서 중복 줄이기
interface SaveAction {
  type: 'save';
}

interface LoadAction {
  type: 'load';
}

type Action = SaveAction | LoadAction;
type ActionType = 'save' | 'load'; // type 반복(하드코딩?)
type ActionType = Action['type'];
  • 매핑된 타입 활용 시 선택적 속성으로 변경 쉬움
interface Options {
  width: number;
  height: number;
  color: string;
  label: string;
}
interface OptionsUpdate {
  width?: number;
  height?: number;
  color?: string;
  label?: string;
}
type OptionsUpdate = { [k in keyof Options]?: Options[k] };

// 표준 라이브러리 Partial
type Partial<T> = { [k in keyof T]?: T[k] };
  • 타입추론된 함수의 반환 값에 명명된 타입 만들기
function getUserInfo(userId: string) {
  // ...
  return { userId, name, age };
  // 추론된 반환 타입: { userId: string; name: string; age: number }
}

type UserInfo = ReturnType<typeof getUserInfo>;
  • 제네릭 타입은 타입을 위한 함수와 같다. 함수는 코드에 대한 반복을 줄여줌
  • 제네릭 타입에서 매개변수를 제한할 수 있는 방법이 필요하다.
    • extends 를 통해 매개변수가 특정 타입을 확장한다고 하면 된다.
interface Name {
  first: string;
  last: string;
}
type DancingDuo<T extends Name> = [T, T];

// 오류나는 코드
const couple1: DancingDuo<Name> = [{ first: 'Fred' }, { first: 'Ginger' }];
// 실제 값이 Name타입의 상위집합임

아이템15 동적으로 인덱스 시그니처 활용하기

  • 인덱스 시그니처 키의 타입은 string, number, symbol이 가능. 하지만 주로 string 사용
  • 인덱스 시그니처의 단점
    • 모든 키를 허용
    • 빈 객체도 유요한 타입이 됨
    • 키마다 다른 타입을 가질 수 없음
    • 타입스크립트의 언어서비스(자동완성)를 사용할 수 없음 (어떠한 키도 모두 가능하기 때문)
  • 그러면 언제 인덱스 시그니처를 사용하기에 적절한가
    • 키의 이름이 무엇인지 알 필요가 없을 때
function parseCSV(input: string): { [columnName: string]: string }[] {
  const lines = input.split('\n');
  const [header, ...rows] = lines;
  return rows.map((rowStr) => {
    const row: { [columnName: string]: string } = {};
    rowStr.split(',').forEach((cell, i) => {
      row[header[i]] = cell;
    });
    return row;
  });
}
  • 연관 배열의 경우, 객체에 인덱스 시그니처를 사용하는 것 대신 Map타입의 사용을 고려할 수 있다. 이는 프로토타입 체인과 관련된 유명한 문제를 우회한다. 아이템 58 참고
    • 아이템 58의 내용
    • 연관 배열이란? key, value로 묶인 배열?
    • 문자열이 주어지고 단어마다 쓰인 횟수를 저장하는 연관 배열이 필요하다고 가정
    • 문자열에 'constructor' 같이 Object.prototype에 있는 단어가 들어가면 의도하지 않은 결과가 나올 수 있음.
    • 따라서 이런 경우 Map을 사용하자(자바스크립트 관련 지식인듯)
function countWords(text: string) {
  const counts = {[word: string]: string} = {};
  for (const word of text.split(/[\s,.]+/)) {
    counts[word] = 1 + (counts[word] || 0);
  }
  return counts;
}

console.log(countWords('Object have a constructor'));

// 실행결과
{
  Objects: 1,
  have: 1,
  a: 1,
  constructor: "1function Object() { [native code] }",
}
  • 레코드와 매핑된 타입을 사용하면 인덱스 시그니처보다 더 정확하게 타입을 표현할 수 있음
type Vec3D = Record<'x' | 'y' | 'z', number>;
type Vec3D = { [k in 'x' | 'y' | 'z']: number };
type Vec3D = {
  x: number;
  y: number;
  z: number;
};

// 매핑된 타입은 키마다 별도의 타입 지정 가능

type ABC = { [k in 'a' | 'b' | 'c']: k extends 'b' ? string : number };
type ABC = {
  a: number;
  b: string;
  c: number;
};

아이템16 number 인덱스 시그니처 대신 Array, 튜플, ArrayLike 사용하기

  • number로 키의 타입을 정의해도 어차피 런타임에 자바스크립트는 키를 string으로 바꾼다. 이걸 꼭 알아두자
  • for in 문은 for of 문보다 몇 배는 느리다. for in 문은 프로토타입 체인을 따라가게 될 수도 있으며, 모든 열거가능 속성을 반복하므로 불확실한 타입이 발생할 수 있다.
  • Array 타입은 순서가 있는 요소들의 배열을 나타내는 반면, ArrayLike 타입은 순서가 있는 요소들의 배열과 유사하지만 배열이 아닌 타입을 나타낸다. ArrayLike는 배열 메서드를 사용할 수 없으므로, 배열과는 다른 점이 있음

결론: number 인덱스 시그니처 쓰지말고, Array, ArrayLike 쓰자

아이템17 변경 관련 오류를 방지하는 readonly

  • readonly는 배열 또는 튜플 리터럴에만 붙일 수 있음을 알자.
  • 자바스크립트에서 원본을 변경하는 메서드 등이 있음.
  • 원본 변경이 싫다면 readonly를 붙여서 의도치 않은 변경을 막을 수 있다.
function arraySum(arr: readonly number[]) {
  let sum = 0,
    num;
  while ((num = arr.pop()) !== undefined) {
    // ~~~ 'pop' does not exist on type 'readonly number[]'
    sum += num;
  }
  return sum;
}
  • number[] 은 readonly number[] 보다 기능이 많기 때문에(확장되었기 때문에) readonly number[] 타입의 서브타입이다.
    • number[] 가 readonly number[] 보다 변경 가능이라는 기능이 더 있다고 이해했음
  • 따라서 readonly number[] 는 number[]에 할당할 수 없다
    • 위 문장으로 이해해도 되지만, 변경될 수 있는 number[]에 변경 불가능한 readonly number[]를 할당하는 것은 당연히 불가능
  • 함수의 매개변수를 readonly로 선언하면 호출하는 쪽에서 함수에 readonly 타입을 전달할 수 있다.
    • readonly 불편하지만 인터페이스를 명확히 하고 타입 안전성을 높일 수 있다.
  • readonly는 얕게 동작한다.
// 두 가지의 차이 이해하기
(readonly string[])[] // readonly 배열의 변경 가능한 배열
readonly string[][] // 변경 가능한 배열의 readonly 배열
interface Outer {
  inner: {
    x: number;
  };
}
const o: Readonly<Outer> = { inner: { x: 0 } };
o.inner = { x: 1 };
// ~~~~ Cannot assign to 'inner' because it is a read-only property
o.inner.x = 1; // OK
  • DeepReadonly 제네릭이 있음. 그걸 가져다 쓰자.

아이템18 매핑된 타입을 사용하여 값을 동기화하기

  • 매핑된 타입을 사용해 타입스크립트가 코드에 제약을 강제하도록 할 수 있다.
interface ScatterProps {
  // The data
  xs: number[];
  ys: number[];

  // Display
  xRange: [number, number];
  yRange: [number, number];
  color: string;

  // Events
  onClick: (x: number, y: number, index: number) => void;
}

// 너무 렌더링이 많이 일어나는 문제
// 보수적, 실패에 닫힌 접근법
function shouldUpdate(oldProps: ScatterProps, newProps: ScatterProps) {
  let k: keyof ScatterProps;
  for (k in oldProps) {
    if (oldProps[k] !== newProps[k]) {
      if (k !== 'onClick') return true;
    }
  }
  return false;
}

// 렌더링이 누락되는 문제
// 실패에 열린 접근법
function shouldUpdate(oldProps: ScatterProps, newProps: ScatterProps) {
  return (
    oldProps.xs !== newProps.xs ||
    oldProps.ys !== newProps.ys ||
    oldProps.xRange !== newProps.xRange ||
    oldProps.yRange !== newProps.yRange ||
    oldProps.color !== newProps.color
    // (no check for onClick)
  );
}

// 새로운 속성이 추가 될 때 변경을 강제하고 싶다...
// 아래는 매핑된 타입과 객체를 활용해 타입 체커가 동작하도록 수정한 코드!
const REQUIRES_UPDATE: { [k in keyof ScatterProps]: boolean } = {
  xs: true,
  ys: true,
  xRange: true,
  yRange: true,
  color: true,
  onClick: false,
};

function shouldUpdate(oldProps: ScatterProps, newProps: ScatterProps) {
  let k: keyof ScatterProps;
  for (k in oldProps) {
    if (oldProps[k] !== newProps[k] && REQUIRES_UPDATE[k]) {
      return true;
    }
  }
  return false;
}
©2024 dlwl98
github
PostsAbout