TypeScript에서 Promise는 Promise<T> 타입을 가지며, T는 Promise가 이행되는 값의 타입입니다. Promise를 반환하는 함수는 반환 타입에 이를 선언하고, then이나 await로 얻는 값은 T 타입입니다.
Promise의 동작 원리(상태, 마이크로태스크 큐, 체이닝)는 평범한 JavaScript이며 JavaScript Promise에서 다룹니다. 이 페이지는 타입에 관한 내용입니다.
new Promise와 resolve에 타입 지정하기
new Promise가 반환 타입이 선언된 함수의 반환값이라면 T는 그 반환 타입에서 옵니다. 단독으로 쓰면 TypeScript는 resolve 호출에서 T를 추론하지 않으므로 Promise<unknown>이 됩니다. 타입 인수를 명시적으로 넘기세요.
세부 사항이 두 가지 있습니다. resolve는 T에서 타입을 가져오므로 Promise<number>에서 resolve("42")는 컴파일 오류입니다. 그리고 인수 없는 resolve()는 T에 void가 포함될 때만 허용됩니다. Promise<number>에서는 TS2794 오류(Expected 1 arguments, but got 0. Did you forget to include 'void' in your type argument to 'Promise'?)가 납니다.
reject 쪽은 타입이 없습니다. Promise<T, E> 같은 것은 없으며, Promise는 어떤 값으로든 거부될 수 있습니다.
then, catch, finally
각 then은 콜백이 반환하는 것으로 타입이 정해지는 새 Promise를 반환합니다. 콜백이 Promise를 반환하면 TypeScript가 풀어 주므로 Promise<Promise<T>>가 생기는 일은 없습니다.
표준 라이브러리에서 catch 콜백의 매개변수는 unknown이 아니라 any입니다. 문자열일 수도 있는 값에 e.message를 써도 아무것도 막지 않습니다. 위처럼 unknown으로 지정하고 사용하기 전에 좁히세요. await를 감싼 try/catch가 더 낫습니다. strict에서는 catch 변수가 이미 unknown입니다(에러 처리 참고).
Promise.all은 튜플을 반환한다
서로 다른 Promise 타입의 배열 리터럴을 넘기면 Promise.all은 튜플의 Promise를 반환하며, 각 위치는 자신의 타입을 유지합니다.
Promise가 아닌 값을 섞어 넣을 수도 있으며, 그 값은 그대로 통과합니다. Promise.all은 입력 하나가 거부되는 즉시 거부되고, 다른 결과는 잃어버립니다. Promise.race는 가장 먼저 처리된 것으로 이행되거나 거부되며 입력들의 유니언으로 타입이 지정됩니다. Promise.any는 처음 성공한 것으로 이행되고, 모든 입력이 실패할 때만 AggregateError로 거부됩니다.
Promise.allSettled와 결과 타입
Promise.allSettled는 모든 입력을 기다리며 절대 거부되지 않습니다. 각 결과는 PromiseSettledResult<T>이고, status 필드로 좁히는 유니언입니다.
거부된 변형에는 value가 없으므로 r.value는 status 확인 뒤에만 존재합니다. reason은 catch와 같은 이유로 any입니다. filter는 타입 서술어를 써서 걸러낸 배열이 이행된 결과로 타입 지정되게 합니다.
콜백 API를 Promise로 감싸기
오래된 API는 결과를 콜백으로, 흔히 Node 스타일 (err, result) => void로 알려 줍니다. 타입이 있는 Promise를 반환하는 함수로 한 번 감싸 두면 나머지 코드는 await를 쓸 수 있습니다.
Node에서는 (err, result) 관례를 따르는 함수에 대해 util.promisify가 이 일을 해 주며, 많은 내장 모듈에 이미 Promise 버전(node:fs/promises, node:timers/promises)이 있습니다.
흔한 실수
- 시그니처가
Promise<T>인데T를 반환하거나, 그 반대.Promise<User>로 선언한 async가 아닌 함수는 Promise를 반환해야 합니다.async함수는 자동으로 Promise를 반환합니다. - Promise 처리를 잊음.
await,then,catch없이save(user);처럼 호출해도 컴파일은 되지만, 거부되면 처리되지 않은 거부가 됩니다(Node 프로세스는 기본적으로 종료됩니다). typescript-eslint의no-floating-promises규칙이 이를 잡아냅니다. .catch((e) => ...)의 타입을 믿음. 그e는any입니다.unknown으로 지정하세요.- 이미 Promise를 반환하는 것을
new Promise로 감쌈. 기존 Promise를 그냥 반환하거나await하세요.
자주 묻는 질문
TypeScript에서 Promise<T>란 무엇인가요?
Promise<T>는 T 타입의 값으로 이행되는 Promise의 타입입니다. Promise<string>을 반환하는 함수는 then 콜백이나 await로 string을 주는 Promise를 돌려줍니다. 값 없이 이행되는 Promise는 Promise<void>입니다.
TypeScript에서 new Promise에 타입을 지정하려면 어떻게 하나요?
타입 인수를 넘깁니다: new Promise<number>((resolve, reject) => ...). 타입 인수가 없으면 TypeScript는 resolve 호출에서 값을 추론하지 못하고 결과는 Promise<unknown>이 됩니다. 아무것도 없이 이행되는 Promise라면 인수 없는 resolve()가 허용되도록 new Promise<void>(resolve => ...)를 쓰세요.
Promise의 catch에서 에러는 어떤 타입인가요?
any입니다. 무엇이든 throw하거나 reject할 수 있으므로 표준 라이브러리에서 catch 콜백의 reason 매개변수는 any로 타입 지정되어 있습니다. 직접 unknown으로 지정하고(.catch((e: unknown) => ...)), 사용하기 전에 instanceof Error로 좁히세요.
TypeScript에서 Promise.all의 타입은 어떻게 동작하나요?
배열 리터럴에 Promise.all을 쓰면 입력마다 요소가 하나씩 순서대로 있는 튜플 타입을 반환합니다. Promise.all([getUser(), getCount()])는 Promise<[User, number]>이므로 구조 분해하면 각 값이 자신의 타입을 가집니다. 같은 타입의 배열 T[]에는 Promise<T[]>를 반환합니다.
Promise.all과 Promise.allSettled의 차이는 무엇인가요?
Promise.all은 입력 중 하나라도 거부되면 바로 거부됩니다. Promise.allSettled는 항상 이행되며, PromiseSettledResult<T> 객체의 배열을 줍니다: { status: "fulfilled", value } 또는 { status: "rejected", reason }. 각 항목은 status를 확인해서 좁히세요.