Dev/etc.

이펙티브 타입스크립트 아이템 11 ~ 20

takeU 2023. 6. 12. 13:28
반응형

아이템 11 - 잉여 속성 체크의 한계 인지하기

  • 구조적 타입 시스템에서 발생할 수있는 오류를 잡을 수 있도록 '잉여 속성 체크' 수행
    • 객체 리터럴을 변수에 할당할 때
    • 함수에 매개변수로 전달할 때
  • 잉여 속성 체크는 할당 가능 검사와는 별도의 과정
  • 타입 단언문을 사용하면 적용되지 않음
  • 인덱스 시그니처를 사용해 속성을 예상할 수 있도록 함
interface Room {
    numDoors: number;
    ceilingHeightFt: number;
}

const r : Room = {
    numDoors: 1,
    ceilingHeightFt: 10,
    elephant: 'present',
} // elephant가 없다고 에러가 발생

구조적 타이핑에 의한 오류

const obj = {
    numDoors: 1,
    ceilingHeightFt: 10,
    elephant: 'present',
}
const r : Room = obj;

obj 타입은 Room타입의 부분 집합이므로 Room에 할당 가능

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

type DiceRollFn = (sides: number) => number;
const rollDice: DiceRollFn = sides => { };
  • 불필요한 코드의 반복을 줄임
  • 반복되는 함수 시그니처를 하나의 함수 타입으로 통합
  • 함수의 매개변수에 타입 선언을 하는 것 보다, 함수 표현식 전체 타입을 정의하는 것이 안전

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

type Tstate = {
  name: string;
  capital: string;
}

interface Istate {
  name: string;
  capital: string;
}

C#에서 비롯된 네이밍, 단순히 차이를 보여주기 위해 작성했으므로 이런 스타일의 네이밍은 지양해야 함

공통

  • 인덱스 시그니처([key: string]: string)
  • 함수타입 ((x: number) => string;)
  • 제네릭 (type TPair<T>)
  • 타입 별칭(alias)

차이

  • 인터페이스, 타입 서로 확장 가능
    • 인터페이스는 유니온 타입 같은 복잡한 타입은 확장할 수 없음
interface IStateWithPop extends TState {
  population: number;
}

type TStateWithPop = IState & { population: number; }

타입

  • 유니온 타입만 존재 (type AorB = 'a' | 'b')
  • 튜플과 배열 타입 간결하게 표현
    • 인터페이스로 비슷하게 구현할 수 있으나, 비효율적
    • concat과 같은 메서드를 사용할 수 없음

인터페이스

  • 보강(argument) 가능
  • 선언 병합(declaration merging)이라함
interface IState {
  a: string;
}

interface IState {
  b: string;
}

const c: IState = {
  a: 'a',
  b: 'b',
} // 정상

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

  • DRY (don’t repeat yourself) 원칙에 따라 함수 뿐만 아니라 타입 정의도 사용할 수 있다.

반복을 제거하는 다양한 방법들

  • 같은 함수 타입 시그니처를 사용할 경우의 타입 분리
type HTTPFnction = (url: string, options: Options) => Promise<Response>;
const get: HTTPFunction = (url, options) => { /* ... */ };
const post: HTTPFunction = (url, options) => { /* ... */ };
  • 인터페이스 확장을 통해 반복 제거
interface Person {
  firstName: string;
  lastName: string;
}

interface PersonWithBirthDate extends Person {
  birth: Date;
}
  • 인터섹션 연산을 통해 속성을 추가하여 확장
interface Person {
  firstName: string;
  lastName: string;
}
type PersonWithBirthDate = Person & { birth: Date };

부분집합일 경우의 중복제거방법

  • 인덱싱을 통해 타입 중복 제거
interface State {
  userId: string;
  pageTitle: string;
  recentFiles: string[];
  pageContents: string;
}

type TopNavState = {
  userId: State['userId'];
  pageTitle: State['pageTitle'];
  recentFiles: State['recentFiles'];
};
  • 좀 더 발전된 매핑된 타입 형태 ⇒ 배열 필드를 루프 도는 방식
type TopNavState = {
  [k in 'userId' | 'pageTitle' | 'recentFiles']: State[k]
};
  • 위의 같은 패턴 라이브러리를 Pick이라고 하며, Pick은 제네릭 타입이다.
// type Pick<T, K> = { [k in K]: T[k] };
type TopNavState = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;

Pick

  • 타입의 반복없이 원하는 속성을 정의할 수 있다.
interface SaveAction {
  type: 'save';
  // ...
}
interface LoadAction {
  type: 'load';
  // ...
}

type Action = SaveAction | LoadAction;
type ActionType = Action['type'] // 'save' | 'load'
type ActionRec = Pick<Action, 'type'>;  // {type: "save" | "load"}

Partial

  • 매핑된 타입을 순회하면서 해당 속성이 있는지 찾아 선택적으로 각 속성을 만듦.
  • 특정 타입의 부분 집합을 만족하는 타입을 정의.
interface Address {
  email: string;
  address: string;
}

type MyEmail = Partial<Address>;
/* 
interface MyEmail{
  email?: string;
  address?: string;
}
*/

const me: MyEmail = {}; // 가능
const you: MyEmail = { email: "noh5524@gmail.com" }; // 가능
const all: MyEmail = { email: "noh5524@gmail.com", address: "secho" }; // 가능

typeof 연산자

  • 값의 형태에 해당하는 타입을 정의하고 싶을 때 사용
const INIT_OPTIONS = {
  width: 640,
  height: 480,
  color: '#00FF00',
  label: 'VGA',
};

/* 이렇게 재정의를 안하기 위해 typeof 연산자 사용
interface Options {
  width: number;
  height: number;
  color: string;
  label: string;
}
*/

type Options = typeof INIT_OPTIONS;

ReturnType

  • 함수 Type의 리턴 타입으로 구성된 타입을 생성
function getUserInfo(userId: string) {
  // COMPRESS
  const name = 'Bob';
  const age = 12;
  const height = 48;
  const weight = 70;
  const favoriteColor = 'blue';
  // END
  return {
    userId,
    name,
    age,
    height,
    weight,
    favoriteColor,
  };
}
// Return type inferred as { userId: string; name: string; age: number, ... }

type UserInfo = ReturnType<typeof getUserInfo>;
// Redux
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>

// store.getState가 값이기 때문에 typeof로 사용해서 제네릭에 타입으로써 넣어줌.

extends

  • 제네릭 타입에서 매개변수를 제한할 수 있는 방법
interface Name {
  first: string;
  last: string;
}

type DancingDuo<T extends Name> = [T, T];

const dancingDuo = <T extends Name>(x: DancingDuo<T>) => x;

const couple1 = dancingDuo([
  {first: 'Fred', last: 'Astaire'},
  {first: 'Ginger', last: 'Rogers'}
]);
<T extends string> // T를 string으로 제한
  • pick을 좀 더 정확히 사용할 수 있음.
interface Name {
  first: string;
  last: string;
}
type DancingDuo<T extends Name> = [T, T];
type FirstLast = Pick<Name, 'first' | 'last'>;  // OK
type FirstMiddle = Pick<Name, 'first' | 'middle'>;

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

  • [property: string]: string
  • 잘못된 키를 포함한 모든 키를 허용
  • 특정 키가 필요하지 않음 ({})
  • 키마다 다른 타입을 가질 수 없음
  • 자동완성 미지원

대안

  • Record 사용 - 키 타입에 유연성을 제공하는 제네릭 타입

    • type Vec3D = Record<'x' | 'y' | 'z', number>
  • 매핑된 타입 사용

    • type Vec3D = {[k in 'x' | 'y' | 'z']: number}
  • 런타임 때까지 객체의 속성을 알 수 없을 대 인덱스 시그니처 사용

  • 안전한 접근을 위해 인덱스 시그니처 값 타입에 undefined 추가

  • 정확한 타입을 사용하는 것이 좋음

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

  • 타입스크립트는 자바스크립트의 타입 혼란을 바로잡기 위해 숫자 키를 허용하고, 문자열 키와 다른 것으로 인식함.
    • 자바스크립트에서 키는 무조건 문자열로 인식
  • 하지만 런타임때는 문자열 키로 인식함. ⇒ 타입 체크 시점에서 오류를 잡음.
const xs = [1,2,3]
const keys = Object.keys(xs); // string[]
for (const key in xs) {
    key; // string
    const x = xs[key]; // number
}
  • 인덱스 시그니처에 number를 사용하기보다 Array나 튜플, ArrayLike를 사용하는것이 좋음

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

function arraySum(arr: number[]) {
  let sum = 0, num;
  while ((num = arr.pop()) !== undefined) {
    sum += num;
  }
  return sum;
}

function printTriangles(n: number) {
  const nums = [];
  for (let i = 0; i < n; i++) {
    nums.push(i);
    console.log(arraySum(nums));
  }
}

// JS는 암묵적으로 함수가 매개변수를 변경하지 않는다고 가정하는데, 이는 타입에 문제가 생길 수 있음.
// 따라서 명시적으로 변경하지 않는다는 것을 알려주는 것이 좋음.
function arraySum(arr: readonly number[]) {
  let sum = 0
  for (const num of arr) {
    sum += num
  }
  return sum
}

배열은 readonly 배열보다 기능이 많기 때문에, 서브타입이 된다.

const a: number[] = [1,2,3]
const a: readonly number[] = [1,2,3]
const a: number[] = b // 할당 불가

매개변수를 readonly로 선언하는 경우

  • 매개변수가 함수 내에서 변경되는지 확인
  • 호출하는 쪽에서 함수가 매개변수를 변경하지 않는다는 보장을 받음
  • 호출하는 쪽에서 함수에 readlonly 배열을 매개변수로 넣을 수 있음

어떤 함수를 readonly로 선언하면, 해당 함수를 호출하는 다른 함수들도 readonly로 선언해야 함.
라이브러리의 함수를 호출하는 경우는 타입 단언문을 사용해야 함.

기본적으로 readonly는 shallow하게 동작하며
deep readonly는 제네릭을 만들거나 라이브러리를 사용해야 함

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

// 보수적(conservative) 접근법, 실패에 닫힌(fail close) 접근법
// 정확하지만 너무 자주 그려질 가능성이 있음
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) {
  let k: keyof 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
  }
}
  • 매핑된 타입을 사용해 관련된 값과 타입을 동기화하도록 해야함
  • 인터페이스에 새로운 속성을 추가할 때, 선택을 강제하도록 매핑된 타입을 고려해야 함

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

  • 타입 추론이 된다면 명시적 타입 구문은 필요하지 않음
  • 비구조화 할당문으로 모든 지역 변수의 타입이 추론되도록 함
  • 이상적인 경우 함수/메서드 시그니처에는 타입 구문이 있지만, 함수 내 지역 변수에는 타입 구문이 없음
  • 추론될 수 있는 경우라도 객체 리터럴과 함수 반환에서는 타입 명시 / 오류 방지

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

  • 서로 관련이 없는 두 개의 값을 분리
  • 변수명을 더 구체적으로 지을 수 있음
  • 타입 추론을 향상시키며, 타입 구문이 불필요해짐
  • 타입이 좀 더 간결해짐
  • let 대신 const로 변수를 선언하므로, 타입 체커가 타입 추론이 쉬워짐

출처: 이펙티브 타입스크립트