개발새발

Promise, Awaited 타입 분석하기 본문

Typescript

Promise, Awaited 타입 분석하기

비숑주인 2025. 5. 10. 20:46

JavaScript에서 Promise는 비동기 처리를 위한 기본 객체다. TypeScript는 이 Promise가 반환하거나 체이닝하는 값을 정적으로 추론할 수 있도록 복잡하고 정교한 타입 시스템을 갖추고 있다. 이번 글에서는 Promise, Awaited, 그리고 Promise.all, .then(), .catch()가 어떻게 타입 추론을 수행하는지를 분석해본다.

 

Promise.resolve vs await

const str1 = Promise.resolve('promise'); // Promise<string>
const str2 = await Promise.resolve('promise'); // string

 

  • Promise.resolve는 Promise<T>를 반환한다.
  • await는 Awaited<T>로 타입이 바뀐다.

이때 Awaited<T>의 정의를 살펴보면 다음과 같다

type Awaited<T> =
  T extends null | undefined ? T :
  T extends object & { then(onfulfilled: infer F, ...args: any): any } ?
    F extends (value: infer V, ...args: any) => any ?
      Awaited<V> :
      never :
  T;

규칙 요약

  • 규칙 1: 객체가 아닌 타입(string, number, boolean)은 그대로 반환.
  • 규칙 2: Promise<T> 타입은 내부의 T로 재귀적으로 들어가면서 Awaited<T> 처리.

즉, Awaited<Promise<Awaited<string>>> → Awaited<string> → string.

 

Promise.all의 타입 추론

const all = await Promise.all([
  'string',
  Promise.resolve(123),
  Promise.resolve(Promise.resolve(true)),
]); // [string, number, boolean]

타입 정의

all<T extends readonly unknown[] | []>(
  values: T
): Promise<{ -readonly [P in keyof T]: Awaited<T[P]> }>;

 

  • 각 요소 T[P]에 대해 Awaited를 적용하고,
  • 배열 그대로 매핑하여 [string, number, boolean]이라는 정확한 튜플 타입을 반환한다.

중첩된 Promise<promise>도 자동으로 Awaited가 적용되어 T가 추출된다.

 

then 체이닝의 타입 추론

const chaining = await Promise.resolve('hi')
  .then(() => 123)
  .then(() => true)
  .catch((err) => {
    console.error(err);
  }); // boolean | void
  • Promise.resolve('hi') → Promise<string>
  • 첫 번째 .then(() => 123) → Promise<number>
  • 두 번째 .then(() => true) → Promise<boolean>
  • .catch()가 반환하는 값은 void이므로, 최종 타입은 Promise<boolean | void>
  • await을 붙이면 boolean | void

관련 타입 선언

interface Promise<T> {
  then<TResult1 = T, TResult2 = never>(
    onfulfilled?: (value: T) => TResult1 | PromiseLike<TResult1>,
    onrejected?: (reason: any) => TResult2 | PromiseLike<TResult2>
  ): Promise<TResult1 | TResult2>;

  catch<TResult = never>(
    onrejected?: (reason: any) => TResult | PromiseLike<TResult>
  ): Promise<T | TResult>;
}

 

구조적 타이핑과 thenable 객체

const thenable = {
  then(onfulfilled: (v: number) => void) {
    setTimeout(() => onfulfilled(42), 10);
  },
};

const v = await thenable; // number

 

  • 객체가 then(onfulfilled) 메서드를 가지고 있으면, TypeScript는 이를 PromiseLike로 간주한다.
  • Awaited<T>는 실제 Promise 인스턴스가 아니더라도 해당 구조만 맞으면 thenable 객체로 처리한다.

정리

상황 결과 타입 근거 설명
await Promise.resolve('a') string Awaited<Promise<T>> → Awaited<T>
Promise.all([...]) 튜플 형태로 정확 추론 각 요소에 Awaited 적용
then().then() 마지막 then 반환값 이전 then의 결과 타입 기반 추론
.catch() 실패 시 반환 타입 포함 TResult 추가 → 성공
await thenableObject then의 인수 타입 추론 구조적 타이핑으로 PromiseLike 처리
 

Promise의 타입은 단순한 타입이 아니다. 특히 Awaited, then 체이닝, Promise.all 등에서는 복잡한 조건부 타입과 재귀적인 타입 추론이 동작한다.
Awaited<T>를 분석해보면, 타입스크립트가 비동기 값을 어떻게 정적으로 분석하고 추적하는지를 깊이 이해할 수 있다.

 

구조적 타이핑 덕분에 실제 Promise 인스턴스가 아니어도 thenable이면 await할 수 있다는 점도 기억해두자.

'Typescript' 카테고리의 다른 글

tsc와 tsconfigjson  (0) 2025.05.10
bind 분석하기  (0) 2025.05.10
flat 분석하기  (0) 2025.05.10
reduce 만들기  (0) 2025.04.30
filter 만들기  (0) 2025.04.30