Conditional types + infer to extract a function's return type
Published October 20, 2026
type AsyncReturnType<Fn extends (...args: never[]) => unknown> = Fn extends (
...args: never[]
) => Promise<infer R>
? R
: Fn extends (...args: never[]) => infer R
? R
: never;
export const fetchUser = async (id: string) => ({ id, name: "Ada Lovelace" });
export type FetchedUser = AsyncReturnType<typeof fetchUser>;What
AsyncReturnType<Fn> extracts what Fn actually resolves to: the awaited value for a function returning a Promise, or the plain return type otherwise. FetchedUser shows it applied to an async function — it resolves to the object the promise contains, not Promise<...>.
Why it matters
TypeScript ships ReturnType<Fn>, but for an async function that just gives you Promise<User>, not User — you'd still need to wrap every use site in Awaited<ReturnType<Fn>>. AsyncReturnType folds that into one utility with two conditional branches: check for the Promise<infer R> shape first and unwrap it, and only fall back to a plain infer R if the function isn't async at all. This is the pattern underneath a lot of "give me the shape of what this API call returns" utilities you'd otherwise write by hand per function.
How it works
- The first branch matches functions whose return type fits
Promise<infer R>—Rcaptures whatever's inside the promise. - The second, nested branch handles the non-promise case:
infer Rhere just captures the plain return type. (...args: never[]) => unknownas the constraint (rather than(...args: any[]) => any) keeps the utility from accepting non-function values while still matching functions of any parameter list —never[]is assignable to any parameter tuple because of how function parameter variance works, whereas a concrete parameter type wouldn't be.typeof fetchUserfeeds the actual function's type intoAsyncReturnType; TypeScript resolves the conditional at the type level with no runtime cost.
Gotchas
- Conditional type branches are checked in order — putting the plain
infer Rbranch first would match every function, including async ones, and you'd getPromise<User>back instead ofUser. Order matters whenever branches overlap. - This only unwraps one level of
Promise. A function returningPromise<Promise<T>>(unusual, butPromise.resolve(somePromise)can produce it) would needAwaited<...>layered on top, sinceAwaitedunwraps recursively and this doesn't.
Related
Awaited<T>— TypeScript's built-in recursive promise unwrapper; reach for it directly once you already have the promise type and don't need the "extract from a function" step.- Template literal types +
infer— the same capture mechanism, matching against string patterns instead of a function's shape.