[이펙티브 타입스크립트] - 아이템36 ~ 아이템40

Lee Jeong Min·2022년 3월 22일
0

TypeScript

목록 보기
15/18
post-thumbnail

[아이템36] 해당 분야의 용어로 타입 이름 짓기

타입 이름을 짓는 것 또한 타입 설계에서 중요한 부분이다.

같은 의미에 다른 이름을 붙이기보다, 특별한 의미가 있을 때만 용어를 구분해야 한다.

// 이렇게 짓기 보다
interface Animal {
  name: string;
  endangered: boolean;
  habitat: string;
}

// 이렇게 짓자
interface Animal {
  commonName: string;
  genus: string;
  species: string;
  status: ConservationStatus;
  climates: KoppenClimate[];
}

type ConservationStatus = 'EX' | 'EW' | 'CR' | 'EN' | 'VU' | 'NT' | 'LC';
type KoppenClimate = 'Af' | 'Am' | 'As' | 'Aw' | 'BSh' | 'BSk' | 'BWh' | 'BWk'; // 더 있음..

자체적으로 용어를 만들어내기보다 해당 분야에 이미 존재하는 용어를 사용하자. 이렇게 하면 타입의 명확성을 올릴 수 있다. 좋은 이름은 추상화의 수준을 높이고 의도치 않은 충돌의 위험성을 줄여준다.

[아이템37] 공식 명칭에는 상표를 붙이기

타입스크립트는 구조적 타이핑 때문에 가끔 이상한 결과를 발생시킬 수 있다. (책에서는 Vector2D를 위한 함수에 Vector3D가 들어왔을 때에도 문제 없음을 예시로 가져옴)

→ 이러한 경우 ‘상표(_brand)’를 이용한다. 이 기법은 타입 시스템에서만 동작하여 런타임 오버헤드를 줄이는 효과가 있다. (런타임에 상표를 검사하는 것과 동일한 효과를 얻을 수 있다.)

보통 함수를 만들어서 타입 단언문을 사용하여 그 값의 타입이 상표 타입인지를 확인하는 방법으로 쓴다.

// 이진 탐색시 목록이 정렬되어있는지 확인하는 상표 기법
type SortedList<T> = T[] & { _brand: 'sorted' };

const isSorted = <T>(xs: T[]): xs is SortedList<T> => {
  for (let i = 1; i < xs.length; i++) {
    if (xs[i] < xs[i - 1]) {
      return false;
    }
  }
  return true;
};

5장 any 다루기

[아이템38] any 타입은 가능한 한 좁은 범위에서만 사용하기

// 이것 보다
function f1() {
  const x: any = expressionReturningFoo();
  processBar(x);
}

// 이게 낫다.
function f2() {
  const x = expressionReturningFoo();
  processBar(x as any)
}

이게 더 좋은 이유는 x라는 타입이 processBar의 호출 이후에도 f1에서는 any인 반면 f2에서는 호출할 때만 any타입이 되기 때문이다. 또한 반환타입이 any인 경우 타입 안정성이 나빠지므로 절대 하면 안된다.

function f1() {
  const x = expressionReturningFoo();
  // @ts-ignore
  processBar(x);
  return x;
}

any 대신 오류를 제거하기 위해 @ts-ignore를 사용할 수 있다.

// 이렇게 any를 사용하는 것보다
const config: Config = {
  a: 1,
  b: 2,
  c: {
    key: value
  }
} as any; 

// 이렇게 범위를 좁혀서 사용하는 것이 낫다.
const config: Config = {
  a: 1,
  b: 2,
  c: {
    key: value as any
  }
}

최소한의 범위에만 any를 사용하자!

[아이템39] any를 구체적으로 변형해서 사용하기

// 이렇게 하지 말자
const getLengthBad(array: any) {
  return array.length
}

// 이렇게 하는것이 낫다.
const getLength(array: any[]) {
  return array.length;
}

3가지의 이유로 위보다 아래의 것이 낫다.

  • 함수 내의 array.length 타입 체크
  • 함수의 반환 타입이 any 대신 number로 추론
  • 함수 호출될 때 매개변수가 배열인지 체크

함수의 매개변수가 객체이지만 값을 알 수 없을때 any보다 아래의 것들을 사용하는 것이 좋다.

  • {[key: string]: any}
  • object
  • 객체지만 속성에 접근할 수 없어야 한다면 unknown 타입

또한 함수 자체도 any 타입을 갖는것 보다 반환 타입을 지정해주어서 사용하는 것이 더 구체적인 형태를 사용할 수 있는 것이다. ex) type Fn0 = () => any;

any를 사용할 때 정말로 모든 값이 허용되어야만 하는지 면밀히 검토하고 사용하자.

[아이템40] 함수 안으로 타입 단언문 감추기

내부 로직이 복잡하여 안전한 타입으로 구현하기 어려운 경우, 함수 내부에는 타입 단언을 사용하고 함수 외부로 드러나는 타입 정의를 정확히 명시하는 정도로 끝내는게 낫다.

안에서 예시로 cacheLast라는 함수를 가져와서 다음과 같이 내부에 단언문을 사용하였지만 호출하는 쪽에서 그것을 알지 못하기 때문에 괜찮다고 설명한다.

function cacheLast<T extends Function>(fn: T): T {
  let lastArgs: any[] | null = null;
  let lastResult: any;
  return function (...args: any[]) {
    if (!lastArgs || !shallowEqual(lastArgs, args)) {
      lastResult = fn(...args);
      lastArgs = args;
    }
    return lastResult;
  } as unknown as T;
}

마찬가지로 뒤에 나오는 예제에서도 실제 오류가 아니라는 것을 알고 있는 경우 any로 단언해서 함수 내부에서 정의하는 것을 확인할 수 있다.

→ 타입 선언문은 일반적으로 타입을 위험하게 만들지만 상황에 따라 현실적인 해결책이 되기도 한다. 불가피하게 사용하는 경우, 정확한 정의를 가지는 함수 안으로 숨기도록 한다.

profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글