본문 바로가기
Book Review/Effective Typescript

Day8_Effective Typescript 스터디 (item 25~ 26)

by 예 강 2023. 6. 16.

2022년 12월 9일 글입니다.

요약 1 . 콜백보다 프로미스를 사용하는 게 코드 작성과 타입 추론 면에서 유리하다.
요약 2. 이왕이면 프로미스 생성보단 async, await을 사용하는게 좋고 오류를 제거 할 수 있다.
요약 3. 어떤 함수가 프로미스를 반환한다면 async 로 선언하는 것이 좋다.

Item 25. 비동기 함수에는 콜백대신 async 함수 사용하기

  • 콜백보다 프로미스나 async/await을 사용해야 하는 이유
    1. 콜백보다 프로미스가 코드짜기 더 쉽다.
    2. 콜백보다 프로미스가 타입추론하기 더 쉽다.

1은 그렇다 치고, 2는 왜일까 ?

function timeout(millis: number): Promise<never> {
  return new Promise((resolve, reject) => {
     setTimeout(() => reject('timeout'), millis);
  });
}

async function fetchWithTimeout(url: string, ms: number) {
  return Promise.race([fetch(url), timeout(ms)]);
}
  • Promise.race에서 반환타입은 입력한 값 fetch(url) 과 timeout(ms)의 합집합(union(|))이다.
  • 즉 위의 경우에 Promise<Response|never> 이다.
  • 그러나 timeout의 반환값은 never 즉 공집합이고 공집합과의 합집합(유니온)은 아무런 효과가 없으므로 Promise로 타입추론이 간단해진다.
  • 이런 원리로 프로미스를 사용하면 타입스크립트의 모든 타입추론이 제대로 동작한다고 한다.(헛발질 없이)
  • 하지만 여기서 또 프로미스보단 async/awiat을 사용해야 한다.
    1. 일반적으로 더 간결하고 직관적인 코드가 된다
    2. async 함수는 항상 프로미스를 반환하도록 강제된다.
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';
}
  • 예제는 캐시를 붙이는 코드인데, 예제를 보면 getUser()를 실행했을 떄, success 로 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';
  	const profile = await fetchWithCache(`/user/${userId}`);
  	requestStatus = 'success';
}
  
  • async함수를 사용해서 코드의 구조를 일관되게 했고, async를 이용해 비동기 함수라는 걸 명시했기 때문에 위 코드는 비동기로 동작한다.
  • 그리고 언제나 success로 끝나기 때문에 의도했던 대로 코드가 작동한다.

한줄요약!

콜백 fetch() > Promise 반환 > async/await 을 사용하자!


Item 26. 타입추론에 문맥이 어떻게 사용되는지 이해하기 (Context inference)

요약 1. 타입 추론에서 문맥이 어떻게 쓰이는지 주의하자 (상수로 쓰는지, 변수로 뽑아서 쓰는지)
요약 2. 얕은참조인지, 깊은참조인지 잘보고, 오류가 발생하면 타입선언을 추가해라
요약 3. 상수 단언을 사용하면 정의한 곳이 아니라 사용한 곳에서 오류가 뜨므로 주의해라

type Language = 'JavaScript' | 'TypeScript' | 'Python';
function setLanguage(language: Language) { /* ... */ }
// Parameter is a (latitude, longitude) pair.
function panTo(where: [number, number]) { /* ... */ }

panTo([10, 20]);  // OK

const loc = [10, 20];
panTo(loc);
//    ~~~ Argument of type 'number[]' is not assignable to
//        parameter of type '[number, number]'
  • 타입스크립트는 panTo의 인자로 [number,number] 두개의 인자를 가진다고 추론했는데, 타입넓히기가 동작해서 number[] 배열로 인식되었기 때문에 오류가난다.
  • 이럴땐
  1. as const를 이용해 내부까지 (deeply) 상수라고 명시해준다.
  2. const loc = [10, 20] as const // readonly[10,20] 이라고 추론해버린다.
  • 그런데 이렇게 하면 너무 과하게 추론해서 [number,number] 가 아니라 readonly[10,20]으로 추론해버린다.
  • 그러므로 최선은 panTo에 where시그니처에 readonly [number,number]라고 해주는 수밖에 없다.
  • function panTo(where: readonly [number, number]) { /* ... */ } const loc = [10, 20, 30] as const; // error is really here. panTo(loc);
  • 이외에도 객체 사용 및 콜백 사용할때 타입추론시 타입넓히기가 발생하여 에러가 나는 경우가 많다.
  • 이럴땐
  1. as 단언문 을 사용하기
  2. 타입선언문을 추가하기 (const pt : Point = { ...})

같은 방식으로 해결할 수 있다.