본문 바로가기
Book Review/Effective Typescript

Day7_Typescript Effective 스터디 (item 22~24)

by 예 강 2023. 6. 16.

2022년 12월 9일 글입니다.

Item 22. 타입 좁히기

✔ 요약 1. 타입 좁히기를 이해하자
✔ 요약 2. 분기문, 태그된/구별된 유니온, 사용자 정의 타입가드등 스킬을 익히자

  • 가장 기본적인 null 체크
const el = document.getElementById('foo');   // HTMLElement | null
if(el){     //HTMLElement
el
el.innerHTML = "party".blink();
}else{   //null
el
alert("no element #foo")
}

위 코드는 HTMLElement | null 인 el 의 타입을 if 분기문을 통해서 HTMLElement로 좁힌다.

  • instanceof를 이용한 타입좁히기(narrowing)
function contains(text: string, search: string|RegExp) {
  if (search instanceof RegExp) {
    search  // Type is RegExp
    return !!search.exec(text);
  }
  search  // Type is string
  return text.includes(search);
}

위 코드는 instanceof 가 어떻게 타입인 RegExp를 구분하는 거지? instanceof는 런타임에서 동작하는데?? 라는 의문이 들어서 RegExp를 찾아보니 클래스였다 >< (클래스는 런타임도 , 빌드타임도 동작함)

  • 속성체크를 이용한 타입 좁히기
interface A { a: number }
interface B { b: number }
function pickAB(ab: A | B) {
  if ('a' in ab) {
    ab // Type is A
  } else {
    ab // Type is B
  }
  ab // Type is A | B
}

in은 값이 있는지 비교하기 때문에 타입에 상관없이 타입을 좁힐 수 있다.

  • isArray와 같은 내장함수로 타입 좁히기
function contains(text: string, terms: string|string[]) {
  const termList = Array.isArray(terms) ? terms : [terms];
  termList // Type is string[]
  // ...
}
  • 주의할 점
const el = document.getElementById('foo'); // type is HTMLElement | null
if (typeof el === 'object') {
  el;  // Type is HTMLElement | null
}

위와 같은 방법은 잘못된 방식인데, 자바스클비트에서 typeof null 또한 "object"이기 때문에, object인지 검사하는 방식으로는 타입을 좁힐 수 없다.

function foo(x?: number|string|null) {
  if (!x) {
    x;  // Type is string | number | null | undefined
  }
}

빈문자열 ''와 0 모두 falsy한 값이라 false로 판명되어 타입은 좁혀지지 않는다.

  • 태그된 유니온 / 구별된 유니온
interface UploadEvent { type: 'upload'; filename: string; contents: string }
interface DownloadEvent { type: 'download'; filename: string; }
type AppEvent = UploadEvent | DownloadEvent;

function handleEvent(e: AppEvent) {
  switch (e.type) {
    case 'download':
      e  // Type is DownloadEvent
      break;
    case 'upload':
      e;  // Type is UploadEvent
      break;
  }
}

개발자가 의도적으로 type이란 태그를 붙이는 거다. 의도적으로 타입이란 속성을 만들어 붙여줌으로써, 유니온값을 구별할 수 있어진다.

  • 사용자 정의 타입가드
const jackson5 = ['Jackie', 'Tito', 'Jermaine', 'Marlon', 'Michael'];
function isDefined<T>(x: T | undefined): x is T {
  return x !== undefined;
}
const members = ['Janet', 'Michael'].map(
  who => jackson5.find(n => n === who)
).filter(isDefined);  // Type is string[]

일부러 만든 isDefined라는 사용자정의타입가드를 이용해 타입을 구분한다.
그냥 filter에 who => who!== undefined 를 한다고 해서 undefined 가 좁혀지지는 않기 때문이다.
좁혀지지 않는 이유는 filter함수의 반환값이 undefined가 나올 수 있기 때문
그러니 함수를 이용해 isDefined를 사용해주자.
단 이 타입가드는 undefined가 아니라는 확식이 들 때만 사용할 수 있을 것 같다.


Item 23. 한꺼번에 객체 생성하기

요약 1. 왠만하면 한꺼번에 객체를 생성해라.
요역 2. 객체를 따로 만들어야 한다면 타입 단언문 (as) 나 전개연산자 (...)를 사용해라.

타입스크립트의 타입은 일반적으로 변경되지 않는다는 걸 가정한다. 그래서 왠만하면 객체 생성과 프로퍼티 할당을 한번에 해라

즉 이렇게 하지 말란 소리다

const pt = {}
pt.x = 3;
pt.y = 4 ; // '{}' 형식에 x 와 y값이 없어서 오류가 난다.


const pt={
x = 3, 
y = 4
} //직관적이고 코드도 쉽다.

‼하지만 굳이 객체를 나눠서 만들어야 한다면 아래와 같은 방법을 사용하자.

  • as 타입단언문 사용하기
    const pt ={} as Point;
    pt.x = 3
    pt.y = 4
as 단언문으로 미리 타입을 좁혀주면 에러가 나지 않는다.

```typescript

interface Point { x: number; y: number; }
const pt0 = {};
const pt1 = {...pt0, x: 3};
const pt: Point = {...pt1, y: 4};  // OK

이런식으로 (이렇게 짜는 사람이 있을까?)
전개연산자를 객체의 타입 선언및 생성을 해준다.

  • 객체이 조건부로 속성을 추가하기
declare let hasMiddle: boolean;
const firstLast = {first: 'Harry', last: 'Truman'};
const president = {...firstLast, ...(hasMiddle ? {middle: 'S'} : {})};
const president: {
    middle: string;
    first: string;
    last: string;
} | {
    first: string;
    last: string;
}
  • 앞에서 배운 헬퍼함수를 이용할 수도 있다.
declare let hasMiddle: boolean;
const firstLast = {first: 'Harry', last: 'Truman'};
function addOptional<T extends object, U extends object>(
  a: T, b: U | null
): T & Partial<U> {
  return {...a, ...b};
}

const president = addOptional(firstLast, hasMiddle ? {middle: 'S'} : null);
president.middle  // OK, type is string | undefined

Item 24. 일관성 있는 별칭 사용하기

요약 1. 별칭 (alias)는 타입좁히는 걸 방해한다. 일관성있게 별칭을 사용하자.
요약 2. 비구조화 할당을 이용해서 일관된 이름을 사용하자
요약 3. 함수 호출이 객체 속성 타입 정제를 무효화 할 수 있다. 하지만 타입스크립트는 무효화 하지 않는다는걸 가정한다. 그러니 속성보다 지역변수를 사용해 타입정제를 하자.

interface Coordinate {
  x: number;
  y: number;
}

interface BoundingBox {
  x: [number, number];
  y: [number, number];
}

interface Polygon {
  exterior: Coordinate[];
  holes: Coordinate[][];
  bbox?: BoundingBox;
}
function isPointInPolygon(polygon: Polygon, pt: Coordinate) {
  const box = polygon.bbox;
  if (box) {
    if (pt.x < box.x[0] || pt.x > box.x[1] ||
        pt.y < box.y[1] || pt.y > box.y[1]) {  // OK
      return false;
    }
  }
  // ...
}

위 처럼하면 polygon은 bbox인데, const box 라면서 코드를 읽는 사람에게 오류가 발생한다.

function isPointInPolygon(polygon: Polygon, pt: Coordinate) {
  const {bbox} = polygon;
  if (bbox) {
    const {x, y} = bbox;
    if (pt.x < x[0] || pt.x > x[1] ||
        pt.y < x[0] || pt.y > y[1]) {
      return false;
    }
  }
  // ...
}

이런식으로 객체 비구조화를 사용하면 네이밍도 같아지므로 편안하다.

지역변수에서 잘 동작하지만, 객체 속성에서는 주의해야 하는데,

function fn(p: Polygon) { /* ... */ }

polygon.bbox  // Type is BoundingBox | undefined
if (polygon.bbox) {
  polygon.bbox  // Type is BoundingBox
  fn(polygon);
  polygon.bbox  // Type is still BoundingBox
}

이런 상황에선, fn을 통과하면서 undefined가 나올 수도 있는 상황인데, 이미 if문을 통과했다고 타입스크립트는 undefined가 나올거라고 예상하지 못한다.

  • 타입스크립트는 함수가 타입 정제를 무효화 하지 않는다고 가정하기 때문에.

하지만 그렇다고 bbox를 뽑아서 쓰면 polygon.bbox와 다른 형태를 띌수도 있다.
주소 참조 vs 값 참조는 언제나 주의하자.

이런 오류가 일어날 수 있음을 알고 코딩하는 것과 모르고 코딩하는 것과는 다르니까 알아두자.