[이펙티브 타입스크립트] 아이템13 ~ 아이템16

Yongwoo Cho·2022년 5월 31일
0

TIL

목록 보기
77/98
post-thumbnail

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

타입스크립트에서 명명된 타입을 정의하는 방법은 두 가지가 있다.

type TState = {
  name: string;
  capital: string;
}
interface IState {
  name: string;
  capital: string;
}

공통점

  • 추가 속성과 함께 할당한다면 오류가 발생한다
  • 인덱스 시그니처를 사용할 수 있다.
type Tdict = { [key: string]: string };
interface IDict {
  [key: string]: string;
};
  • 함수 타입을 정의할 수 있다.
type TFn = (x: number) => string;
interface IFn {
  (x: number) => string;
}
  • 제네릭이 가능하다
type TPair<T> = {
  first: T;
  second: T;
}
interface IPair<T> {
  first: T;
  second: T;
}
  • 인터페이스는 타입을 확장할 수 있으며, 타입은 인터페이스를 확장할 수 있다.
interface IStateWithPop extends TState {
  population: number;
}
type TStateWithPop = IState & { population: number; };

IStateWithPop과 TStateWithPop은 동일하다.

❗ 인터페이스는 유니온 타입 같은 복잡한 타입을 확장하지 못한다. 복잡한 타입을 확장하고 싶으면 타입과 &를 사용해야 한다.

  • 클래스를 구현할 수 있다.
class StateT implements TState {
  name: string = '';
  capital: string = '';
}
class StateI implements IState {
  name: string = '';
  capital: string = '';
}

차이점

  • 튜플구현
type Pair = [number, number];

interface Tuple {
  0: number;
  1: number;
  length: 2;
}

❗ 인터페이스로 튜플과 비슷하게 구현하면 튜플에서 사용할 수 있는 concat 과 같은 메서드를 사용할 수 없다. 따라서 튜플은 type 키워드로 구현하자

  • 인터페이스는 선언 병합이 가능하다
interface IState {
  name: string;
}
interface IState {
  capital: string;
}
const ex: IState = {
  name: 'yongwoo';
  capital: 'seoul'
}; // 정상

👉 타입 선언 파일을 작성할 때는 선언 병합을 지원하기 위해 반드시 인터페이스를 사용해야 하며 표준을 따라야 한다.

복잡한 타입이라면 고민할 것도 없이 타입 별칭을 사용한다. 그러나 타입과 인터페이스, 두 가지 방법으로 모두 표현할 수 있는 간단한 객체 타입이라면 일관성과 보강의 관점에서 고려해 봐야 한다.

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

타입 중복은 코드 중복만큼 많은 문제를 발생시키므로 타입에도 DRY원칙을 적용하자

타입에 이름을 붙이자

function distance(a: {x: number, y: number}, b: {x: number, y: number}) { /* ... */ }

interface Point {
  x: number;
  y: number;
}
function distance(a: Point, b: Point){ /* ... */ }

함수가 같은 타입 시그니처를 공유하면 분리하자

function get(url: string, opts: Options): Promise<Response> { /* ... */ }
function post(url: string, opts: Options): Promise<Response> { /* ... */ }

type HTTPfunction = (url: string, opts: Options) => Promise<Response>;
const get: HTTPfunction = (url, opts) => { /* ... */ };
const post: HTTPfunction = (url, opts) => { /* ... */ };

타입확장 시 다른 인터페이스로부터 확장하자

interface Person {
  firstName: string;
  lastName: string;
}
interface PersonWithBirth extends Person {
  birth: Date;
}
  • 기존의 존재하는 큰 집합으로부터 파생되는 타입지정 👉 매핑된 타입 사용하기
interface State {
  userId: string;
  pageTitle: string;
  recentFiles: string[];
  pageContents: string;
}

type TopNavState = {
  [k in 'userId' | 'pageTitle' | 'recentFiles']: State[k]
};

인덱싱을 통해 타입 반복을 줄이자

interface SaveAction {
  type: 'save';
}
interface LoadAction {
  type: 'load';
}
type Action = SaveAction | LoadAction;
type ActionType = 'save' | 'load'; // 👎 타입이 반복됨
type ActionType = Action['type']; // 👍 유니온 인덱싱을 통한 반복제거

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

타입스크립트에서는 타입에 인덱스 시그니처를 명시하여 유연하게 매핑을 표현할 수 있다.

type Rocket = { [property: string]: string };
const rocket: Rocket = {
  name: 'Falcon 9',
  variant: 'v1.0',
  thrust: '4,940 kN',
};

👎 위와 같은 타입 체크의 단점

  • 잘못된 키를 포함해 모든 키를 허용한다. name 대신 name으로 작성해도 유효한 Rocket 타입이 된다.
  • 특정 키가 필요하지 않는다. {}도 유효한 Rocket 타입이다.
  • 키마다 다른 타입을 가질 수 없다.
  • 자동 완성 기능이 동작하지 않는다.
interface Rocket {
  name: string;
  varinat: string;
  thrust: number;
}
const rocket: Rocket = {
  name: 'Falcon 9',
  variant: 'v1.0',
  thrust: 100,
};

위와 같은 단점을 인터페이스로 설정함으로써 해결할 수 있다.

인덱스 시그니처는 동적 데이터(런타임 때까지 객체의 속성을 알 수 없음)를 표현할 때 사용한다.

ex) CSV 파일처럼 헤더 행에 열이름이 있고, 데이터 행을 열 이름과 값으로 매핑하는 객체로 나타내고 싶은 경우
일반적인 상황에서 열 이름이 무엇인지 미리 알 방법이 없다. 따라서 이럴때는 인덱스 시그니처를 사용한다. 알고 있을 때는 미리 선언해 둔 타입으로 단언문을 사용한다.

❗ 어떤 타입에 가능한 필드가 제한되어 있는 경우라면 인덱스 시그니처로 모델링하면 안된다.

string 타입이 너무 광범위해서 인덱스 시그니처를 사용할 때 문제가 있을때 해결방법

  • Record 사용하기
type Ved3D = Record<'x' | 'y' | 'z', number>;
// Type Vec3D = {
//   x: number;
//   y: number;
//   z: number;
// }

Record는 키 타입에 유연성을 제공하는 제너릭 타입이다. 특히, string의 부분 집합을 사용할 수 있다.

  • 매핑된 타입 사용하기
type Ved3D = { [k in 'x' | 'y' | 'z']: number };
// Type Vec3D = {
//   x: number;
//   y: number;
//   z: number;
// }

가능하다면 인터페이스, Record, 매핑된 타입 같은 인덱스 시그니처보다 정확한 타입을 사용하는 것이 좋다.

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

interface Array<T> {
  // ...
  [n: number]: T;
}
const xs = [1, 2, 3];
const x1 = xs['1']; // ❌ 에러 (인덱스 식이 number형식이여야 함)

👍 타입 체크 시점에 오류를 잡을 수 있어서 유용함

배열 순회 방법

  • 인덱스에 신경 쓰지 않을 때 : for-of
  • 인덱스의 타입이 중요할 때 : Array.prototype.forEach
  • 루프 중간에 멈춰야할 때 : for(;;) 루프

❗ 타입이 불확실하다면 for-in 루프는 for-of 또는 C스타일 for 루프에 비해 몇 배나 느림

어떤 길이를 가지는 배열과 비슷한 형태의 튜플을 사용하고 싶다면 타입스크립트에 있는 ArrayLike 타입을 사용한다.

profile
Frontend 개발자입니다 😎

0개의 댓글