[독서] 이펙티브 타입스크립트 아이템 21 - 25

dante Yoon·2021년 9월 12일
0

독서

목록 보기
7/38

독서 후 개인적으로 정리한 내용을 기록한 글입니다.

아이템 21 타입 넓히기

자바스크립트의 런타임에 모든 변수는 유일한 값을 가진다.
타입스크립트의 정적 분석 시점에는 변수는 가능한 값들의 집합인 타입을 가진다.
변수를 상수를 이용해 초기화할 때 타입을 명시하지 않으면 타입 체커가 타입을 결정해야 한다.
대입된 값을 가지고 할당 가능한 값들의 집합을 유추해야 한다는 뜻이다. 이렇게 할당 가능한 값들의 집합을 유추하는 과정을 넓히기 widening라고 부른다.

아래 코드는 런타임에서 아무 문제 없이 작동하는 정상적인 코드이다.

interface Vector3 { x: number; y: number; z: number;}
function getComponent(vector: Vctor3, axis: "x" | "y" | "z"){
	return vector[axis];
}

정적 타입을 제공받는 IDE에서는 오류가 표시된다.

let x = "x";
let vec = {x: 10, y: 20, z:30};
getComponent(vec, x) // string 형식의 인수는 "x" | "y" | "z" 형식의 매개변수에 할당될 수 없습니다. 

let x의 할당 시점에 넓히기가 동작해서 string으로 추론이 되었기 때문에 오류가 발생했다.

넓히기가 진행될 때 주어진 값으로 추론 가능한 타입이 여러 개이기 때문에 어떤 값으로 추론될지 매우 헷갈린다.

const mixed = ["x", 1];
/**
 ("x" | 1)[]
 ["x", 1]
 [string, number]
 readonly [string, number]
 (string|number)[]
 readonly. (string|number)[]
 [any,any]
 any[]
*/

넓히기의 과정을 제어할 수 있는 방법이 있다.

const를 사용하면 let을 사용할 때 보다 더 좁은 타입이 된다.
const로 선언된 상수는 재할당 될 수 없기 때문이다.

const x = "x"; //타입이 "x" 

객체의 경우 타입스크립트의 넓히기 알고리즘은 각 요소를 let으로 할당된 것처럼 다룬다.

const v = {
  x: 1, // {x: number}
};
v.x = "3"; //Type 'string' is not assignable to type 'number'.ts(2322)

타입의 추론의 강도를 제어하기 위해서는 다음의 방법이 있다.

// 1. 명시적 타입 구문을 제공한다. 
const v: {x: 1|3|5 };
// 2. 타입 체커에 추가적인 문맥을 제공한다. 함수의 매개변수로 값을 전달.
// 3. const 단어문을 사용 
const v1 = {
x:1,
y:2
} as const; // 타입은 {readonly x: 1; readonly y: 2}
const a2 = [1,2,3] as const // 타입이 readonly [1,2,3];

아이템 22 타입 좁히기

1.if 문을 이용해 타입을 HTMLElement | null 에서 HTMLElement로 좁힐 수 있다.

2.instanceof를 사용하는 방법도 있다.

function contain(text: string, search: string|RegExp){
  if(search instance of RegExp) {
    return !!search.exec(text) // 타입이 RegExp
  }
  return text.includes(search); // 타입이 string
}

3.속성 체크

interface A {a: number};
interface B {b: number};
function pickAB(ab: A|B) {
  if("a" in ab){
    //타입이 A
  }
  else {
    //타입이 B
  }
  // 타입이 A|B
}
  1. 태그된 유니온 (tagged union), 구별된 유니온 (discriminated union)
interface UploadEvent { type: "upload"; ...}
interface DownloadEvent { type: "download"; ....}
function handleEvent(e: AppEvent){
  switch(e.type){
    case "download":
      // 타입이 DownloadEvent;
    case "upload":
      // 타입이 UploadEvent
  }
}
  1. 사용자 정의 타입 가드 사용
function isInputElement(el: HTMLElement): el is HTMLInputElement {
  return "value" in el;
}
function getElementContent(el: HTMLElement){
  if(isInputElement) {
    ...
  }
}

아이템 23 한꺼번에 객체 생성하기

타입은 한번 정해지면 일반적으로 변경되지 않는다.
그리고 객체지향 프로그래밍에서는 인터페이스의 변경이 이뤄지면 객체의 추상화 및 캡슐화에좋지 않기 때문에 타입의 변경은 지양하는 게 맞다.

const pt ={};
pt.x = 3; // ~ '{}' 형식에 'x'속성이 없습니다.
pt.y = 4; // ~ '{}' 형식에 'y'속성이 없습니다.

객체를 반드시 제각각 나눠서 만들어야 한다면, 타입 단언문(as)를 사용해 타입 체커를 통과하게 할 수 있다.

조건부 속성을 추가하려면 속성을 추가하지 않은 null 또는 빈 객체 리터럴인 {}으로 객체 전개를 사용한다.

let hasMiddle: boolean;
const firstLast = {first: "Harry", last: "Truman"};
const president = {...firstLast, ...(hasMiddle ? {middle: "s"}: {})};

근데 조건부 속성 추가 대상이 두 개 이상의 키 값을 가진 객체는 선택적 타입이 아닌 유니온 타입으로 된다.

let hasDate: boolean;
const nameTitle = {name: "some", title: "some"};
const pharaoh = {
  ...nameTitle,
  ...(hasDates ? {start: -2589, end: -2522}: {}),
}

선택적 필드를 위해서는 다음과 같이 헬퍼 함수를 사용한다.

function addOptional<T extends object, U extends object>(
a:T, b: U | null
 ): T & Partial<U> {
  return {...a, ...b};
}

const pharaoh = addOptional(
  nameTitle,
  hasDates ? {start: -2589, end: -2566}: null
);
pharaoh.start // 정상 타입이 number | undefined 

아이템 24 일관성 있는 별칭 사용하기

loc 이라는 별칭을 만들고 값을 변경하면 brough의 속성 값 또한 변경된다.

const brough = {name: "Brooklyn", location: [40.688, -73.979]};
const loc = brough.location;

별칭의 남발은 제어흐름을 분석하기 어렵게 만든다.

function isPointInPolygon(polygon: Polygon, pt: Coordinate) {
  const box = polygon.bbox; // 타입이 BoundingBox | undefined
  if(polygon.bbox){
    polygon.bbox // 타입이 BoundingBOx
    box // 타입이 BoundingBox | undefined
  }
}

별칭은 일관성 있게 사용한다는 원칙을 지키면 위와 같은 제어흐름 복잡성을 개선할 수 있다.

객체 비구조화를 이용하면 위에서 얻으려는 간편성과 제어흐름 복잡성 개선 모두 이룰 수 있다.

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 < y[0] || pt.y > y[1]}{
      return false;
    }
  }
}

아이템 25 비동기 코드에는 콜백 대신 async 함수 사용하기

모던 자바스크립트에서는 callback 지옥을 개선하기 위해 async, await를 많이 사용한다.

타입스크립트 컴파일러는 ES5 이하 버전을 대상으로 async await을 동작하게 하도록 정교한 변환을 수행한다.

병렬로 페이지를 로드한다고 할때 Promise.all을 사용해서 프로미스를 조합할 수 있다.
세가지 response 변수 각각의 타입을 구조분해 할당과 함께 사용할 수 있다.

async function fetchPages() {
  const [response1, response2, response3] = await Promise.all([
    fetch(url1), fetch(url2), fetch(url3)
    ]);
}

본 책에서는 더 간결하고 직관적인 코드가 되기에 async await을 사용하는 것을 추천한다.
또 async 함수는 항상 프로미스를 반환하도록 강제한다.

// function getNumber(): Promise<number>
async function getNumber(){
  return 42;
}

즉시 사용 가능한 값에도 프로미스를 반환하는 것이 이상하지만, 함수 반환값은 동기, 비동기 둘 중 한가지로 통일이 되어야 하므로 상황에 따라 동기, 비동기 타입을 반환하는 콜백함수보다 사용하기가 용이하다.

다음 콜백함수는 캐시 유무에 따라 동기/비동기의 리턴 타입을 가진다.

const _cache: {[url: string]: string} = {};
function fetchWithCache(url: string, callback: (text: string) => void) {
  if(url in _cache){
    callback(_cache[url]);
  } else {
    fetchURL(url, text => {
      _cache[url] = text;
      callback(text);
    })
  }
}

let requestStatus: "loading" | "success" | "error";
function getUser(userId: string) {
  fetchWithCache(`/user/${userId}`, profile => {
  	requestStatus = "success";
  });
  requestStatus = "loading";
}

캐시가 되어있다면 곧바로 callback 함수를 호출하고 동기적으로 status는 "loading"이 된다.

async await을 사용해보자

const _cache: {[url: string]: string} = {};
async function fetchWithCache(url: string){
  if(url in _cache){
    return _cache[url];
  }
  const response = await fetch(url);
  const text = await response.text();
  _cache[url] = text;
  return text;
}

let requestStatus: "loading" | "success" | "error";
async function getUser(userId: string){
  requestStatus = "loading";
  onst profile = await fetcWithCache(`/user/${userId}`);
  requestStatus = "success";
}

콜백 보다는 프로미스와 async await을 사용하는 것이 코드 작성과 타입 추론 면에서 유리하다.

profile
성장을 향한 작은 몸부림의 흔적들

0개의 댓글