holdanddeepdive / typescript-study

4 stars 0 forks source link

이펙티브 타입스크립트 2장 #19

Open sbyeol3 opened 1 year ago

sbyeol3 commented 1 year ago

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

인터페이스 선언타입 선언의 비슷한 점

  1. 인덱스 시그니처는 인터페이스와 타입에서 모두 사용할 수 있다.
type TDict = {[key: string]: string};
interface IDict = {
  [key: string]: string;
}
  1. 함수 타입도 인터페이스나 타입으로 정의할 수 있다.
type TFn = (x: number) => string;
interface IFn {
  (x: number): string;
}
  1. 타입 별칭과 인터페이스는 모두 제너릭이 가능하다.
type TPair<T> = {
  first: T;
  second: T;
};
interface IPair<T> {
  first: T;
  second: T;
}
  1. 인터페이스는 타입을 확장할 수 있으며, 타입은 인터페이스를 확장할 수 있다.

    인터페이스는 유니온 타입 같은 복잡한 타입을 확장하지 못하므로 이런 경우에는 타입과 &를 사용해야 한다.

interface IStateWithPop extends TState {
  population: number;
}
type TStateWithPop = IState & { population: number };
  1. 클래스를 구현할 때는 타입과 인터페이스 둘 다 사용할 수 있다.
class StateT implements TState {
  name: string = "";
  capital: string = "";
}

class StateI implements IState {
  name: string = "";
  capital: string = "";
}

인터페이스 선언타입 선언의 다른 점

  1. 유니온 타입은 존재하지만 유니온 인터페이스는 존재하지 않는다.
type NamedVariable = (Input | Output) & { name: string };
type Pair = [number, number];
type StringList = string[];
type NamedNums = [string, ...number[]];

// 인터페이스로 튜플 구현
interface Tuple {
  0: number;
  1: number;
  length: 2;
}

const t: Tuple = [10, 20]; // 정상
// 🥲 concat과 같은 메서드는 사용할 수 없음
  1. 인터페이스에는 타입에 없는 선언 병합(declaration merging) 기능을 사용할 수 있다.
interface IState {
  name: string;
  capital: string;
}
interface IState {
  population: number;
}

const wyoming: IState = {
  name: "Wyoming",
  capital: "Cheyenne",
  population: 500000,
};

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

Don't repeat yourself : 같은 코드를 반복하지 말라.

type Pick<T, K> = { [k in K]: T[k] };
type TopNavState = Pick<State, "userId" | "pageTitle" | "recentFiles">;
type OptionsUpdate = { [k in keyof Options]?: Options[k] };
// 흔한 패턴이므로 이미 표준 라이브러리에 Partial로 존재
interface Name {
  first: string;
  last: string;
}
type DancingDuo<T extends Name> = [T, T];

const couple1: DancingDuo<Name> = [
  { first: "Fred", last: "Astaire" },
  { first: "Ginger", last: "Rogers" },
]; // OK
const couple2: DancingDuo<{ first: string }> = [
  { first: "Sonny" },
  { first: "Cher" },
]; // ERROR : { first: string; } 타입은 Name을 확장하지 않음 ❌

type FirstLast = Pick<Name, "first" | "last">; // 정상
type FirstMiddle = Pick<Name, "first" | "middle">; // ERROR : 'middle'을 first' | 'last'에 할당할 수 없음

아이템 15. 동적 데이터에 인덱스 시그니처 사용하기

// EX : CSV 파일처럼 헤더 행에 열 이름이 있고, 데이터 행을 열 이름과 값으로 매핑하는 객체로 나타내고 싶은 경우

function parseCSV(input: string): { [columnName: string]: string }[] {
  const lines = input.split("\n");
  const [header, ...rows] = lines;
  const headerColumns = header.split(",");
  return rows.map((rowStr) => {
    const row: { [columnName: string]: string } = {};
    rowStr.split(",").forEach((cell, i) => {
      row[headerColumns[i]] = cell;
    });
    return row;
  });
}

// 열 이름이 무엇인지 미리 알 방법이 없으므로 인덱스 시그니처 사용
interface Row1 {
  [column: stirng]: number;
} // 너무 광범위

interface Row1 {
  a: number;
  b?: number;
  c?: number;
  d?: number;
} // 최선

type Row3 =
  | { a: number }
  | { a: number; b: number }
  | { a: number; b: number; c: number }
  | { a: number; b: number; c: number; d: number }; // 번거로움
// 1. Record 사용
type Vec3D = Record<"x" | "y" | "z", number>;

// 2. 매핑된 타입 사용
type Vec3D = { [k in "x" | "y" | "z"]: number };
type Vec3D = { [k in "x" | "y" | "z"]: k extends "b" ? string : number }; // a: number; b: string; c:number;

아이템 16. number 인덱스 시그니처보다는 Array, 튜플, ArrayLike를 사용하기

// Array에 대한 타입 선언
interface Array<T> {
  // ...
  [n: number]: T;
}

아이템 17. 변경 관련된 오류 방지를 위해 readonly 사용하기

매개변수를 readonly로 설정하면?

👀 readonly는 얕게 동작한다! 깊게 동작하려면 ts-essentials의 DeepReadonly 제너릭을 사용해야 한다.

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