지식
TypeScript
Awaited 유틸리티 타입 완전 정복
TypeScript 4.5에 도입된 Awaited<T>가 Promise를 재귀적으로 언래핑하는 원리를 설명합니다. async 함수 반환 타입 추출, Promise.all 결과 타입, React Query와의 통합, 고차 비동기 함수 패턴을 정리합니다.
지난 글에서 함수 타입을 분해하는 방법을 살펴봤습니다. 이번에는 비동기 함수와 필연적으로 만나는 Awaited<T>를 다룹니다. TypeScript 4.5에서 공식 추가된 이 유틸리티는 Promise를 재귀적으로 언래핑해서 실제 값 타입을 꺼내 줍니다.
왜 Awaited가 필요한가
TypeScript 4.5 이전에는 Promise<T>에서 T를 꺼내려면 번거로운 수동 타입 조작이 필요했습니다.
// 이전 방법 (TypeScript < 4.5)
type Unwrap<T> = T extends Promise<infer V> ? V : T;
// 하지만 Promise<Promise<number>>는 Promise<number>로만 풀리고 number로 안 풀림
// Awaited는 재귀적으로 처리
type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<Promise<number>>>; // number (재귀!)
type C = Awaited<string>; // string (비-Promise는 그대로)
내부 구현
// TypeScript 4.5+ 표준 라이브러리
type Awaited<T> =
T extends null | undefined
? T
: T extends object & { then(onfulfilled: infer F, ...args: infer _): any }
? F extends (value: infer V, ...args: infer _) => any
? Awaited<V> // 재귀: then 콜백의 인자 타입을 다시 Awaited로
: never
: T;
세 가지 경우를 처리합니다:
null | undefined→ 그대로 반환thenable객체 →then콜백의 첫 번째 인자 타입을 재귀적으로 Awaited- 나머지 → 그대로 반환
Promise<T>만 처리하는 게 아니라 then 메서드가 있는 thenable 객체 전체를 처리한다는 점이 중요합니다.
핵심 패턴: Awaited + ReturnType
가장 자주 쓰이는 조합은 Awaited<ReturnType<typeof asyncFn>>입니다.
// API 함수
async function fetchUserProfile(userId: string) {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error("Not found");
return res.json() as Promise<{
id: string;
name: string;
avatar: string;
plan: "free" | "pro";
}>;
}
// 반환 타입 추출 — 수동으로 interface를 선언할 필요 없음
type UserProfile = Awaited<ReturnType<typeof fetchUserProfile>>;
// → { id: string; name: string; avatar: string; plan: "free" | "pro" }
이 패턴의 이점은 fetchUserProfile의 반환 타입이 바뀌면 UserProfile도 자동으로 갱신된다는 점입니다.
Promise.all과 함께
async function loadDashboard(userId: string) {
const [user, posts, stats] = await Promise.all([
fetchUser(userId),
fetchPosts(userId),
fetchStats(userId),
]);
return { user, posts, stats };
}
// Promise.all 결과 타입 추출
type DashboardData = Awaited<ReturnType<typeof loadDashboard>>;
// → { user: User; posts: Post[]; stats: Stats }
실전 패턴
제네릭 비동기 유틸리티 타입
// 비동기 함수의 반환 타입을 간결하게 추출
type AsyncReturnType<F extends (...args: any[]) => Promise<any>> =
Awaited<ReturnType<F>>;
type UserData = AsyncReturnType<typeof fetchUser>;
type ProductData = AsyncReturnType<typeof fetchProduct>;
React Query와의 통합
// queryFn의 반환 타입을 자동으로 data에 반영
function useUserProfile(userId: string) {
return useQuery({
queryKey: ["user", userId],
queryFn: () => fetchUserProfile(userId),
});
}
// data의 타입은 자동으로 UserProfile | undefined
const { data } = useUserProfile("123");
// data.plan — "free" | "pro" | undefined (완전 타입 추론)
재시도 래퍼
async function withRetry<T>(
fn: () => Promise<T>,
maxAttempts = 3,
delay = 1000
): Promise<T> {
let lastError: unknown;
for (let i = 0; i < maxAttempts; i++) {
try {
return await fn();
} catch (e) {
lastError = e;
if (i < maxAttempts - 1) {
await new Promise(r => setTimeout(r, delay * (i + 1)));
}
}
}
throw lastError;
}
// 원래 함수의 반환 타입을 보존
const user = await withRetry(() => fetchUser(id));
// user: User — Promise가 아닌 실제 타입
Awaited vs infer R
직접 infer를 쓸 때와 비교해보면 Awaited가 훨씬 간결합니다.
// infer를 직접 쓰는 경우 — 재귀가 없어서 깊은 Promise에서 실패
type ShallowUnwrap<T> = T extends Promise<infer R> ? R : T;
type X = ShallowUnwrap<Promise<Promise<number>>>; // Promise<number> — 한 겹만 벗김
// Awaited — 재귀적으로 완전히 언래핑
type Y = Awaited<Promise<Promise<number>>>; // number
Thenable 처리
Awaited는 네이티브 Promise가 아닌 thenable 객체도 처리합니다.
// 커스텀 thenable (예: RxJS Observable의 toPromise)
type MyThenable<T> = { then(resolve: (value: T) => void): void };
type A = Awaited<MyThenable<string>>; // string
type B = Awaited<MyThenable<MyThenable<number>>>; // number
이는 다양한 비동기 라이브러리와의 호환성을 위한 설계입니다.
지난 글: ReturnType과 Parameters로 함수 타입 분해하기
다음 글: NonNullable 유틸리티 타입 완전 정복
읽어주셔서 감사합니다. 😊