Skip to content

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

@sbyeol3

Description

@sbyeol3

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

  • 대부분의 경우 타입과 인터페이스 모두 사용해도 되지만 차이를 아고, 일관성을 유지해야 한다.
  • I T 접두사를 붙이는 네이밍 방식은 지양해야 한다. (C#에서 유래)

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

  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 키워드는 쓰임새가 더 많다.
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 : 같은 코드를 반복하지 말라.

  • 확장을 사용하여 반복을 제거할 수 있다.
  • 이미 존재하는 타입을 확장하는 경우에, 일반적이지 않지만 인터색션 연산자를 사용할 수도 있다.
  • 매핑된 타입은 배열의 필드를 루프 도는 것과 같은 방식이다. 이 패턴은 표준 라이브러리에서도 일반적으로 찾을 수 있으며, Pick이라고도 한다.
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로 존재
  • 제너릭 타입은 타입을 위한 함수와 같다. 타입을 반복하는 대신 제너릭 타입을 사용하여 타입들 간에 매핑을 하는 것이 좋다.
  • 제너릭 타입에서 매개변수를 제한할 수 있는 방법은 extends를 사용하는 것이다.
  • 표준 라이브러리에 정의된 Pick, Partial, ReturnType 같은 제너릭 타입에 익숙해져야 한다.
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 }; // 번거로움
  • 인덱스 시그니처를 사용하기에는 string 범위가 광범위 하다면 두 가지 대안이 있다.
// 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;
}
  • 어떤 길이를 가지는 배열과 비슷한 형태의 튜플을 사용하고 싶다면 타입스크립트에 있는 ArrayLike 타입을 사용한다.

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

  • readonly number[]는 타입이다. 배열의 요소를 읽을 수 있지만, 쓸 수는 없으며 length를 변경할 수 없다. 또한 pop을 비롯한 배열을 변경하는 다른 메서드를 호출할 수 없다.

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

  • 타입스크립트는 매개변수가 함수 내에서 변경이 일어나는지 체크한다.
  • 호출하는 쪽에서는 함수가 매개변수를 변경하지 않는다는 보장을 받는다.
  • 호출하는 쪽에서 함수에 readonly 배열을 매개변수로 넣을 수도 있다.

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

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

  • 인터페이스 새로운 속성을 추가할 때, 선택을 강제하도록 매핑된 타입을 고려해야 한다.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions