Typing a helper that accepts either a value or a Promise of it
A cache getter may already hold the value, or may still be loading it. Implement resolve so it accepts either shape and always hands the caller a settled value of the same type.
Constraints: one generic parameter; a caller writing await resolve(x) must get T, never T | Promise<T>; no as assertions and no any. Assume input is never a thenable that is not a real Promise.
type MaybePromise<T> = T | Promise<T>;
async function resolve<T>(input: MaybePromise<T>): Promise<T> {
// your code here
}
Write the implementation.
Take T | Promise<T> in and hand Promise<T> back. Inside an async function await input normalizes both shapes to T, and even a bare return input type-checks, because an async return position already accepts a value or a thenable. What you must not do is leak T | Promise<T> outward — a union return forces every caller to branch, while Promise<T> lets them simply await.
- ✗Letting
T | Promise<T>escape as the return type, pushing the branch onto callers - ✗Believing
awaiton a non-promise value is an error rather than a no-op - ✗Expecting an
asyncfunction to double-wrap a promise it returns
- →How does
Awaited<T>change this signature whenTmay itself be aPromise? - →Why does
return inputtype-check even thoughinputmay not be aPromise?
The solution
type MaybePromise<T> = T | Promise<T>;
async function resolve<T>(input: MaybePromise<T>): Promise<T> {
return await input;
}
One line — and the whole point is that await on a non-promise is not an error: it simply yields the value (formally, it passes it through one microtask). The type of await over T | Promise<T> is Awaited<T | Promise<T>>, which for a non-promise T collapses to T.
A bare return input type-checks too, because the return position of an async function typed Promise<T> accepts T | PromiseLike<T>:
async function resolve<T>(input: MaybePromise<T>): Promise<T> {
return input; // also valid
}
What you must not do
// ❌ the union leaks outward — every caller now has to branch
function resolveBad<T>(input: MaybePromise<T>): MaybePromise<T> {
return input;
}
const v = resolveBad(maybe);
if (v instanceof Promise) { /* ... */ } else { /* ... */ } // ❌ at every call site
MaybePromise<T> belongs on the input and hurts on the output: asynchrony is contagious, and the helper's job is to absorb the uncertainty, not to propagate it.
⚠️ If T may itself be a promise, the precise return type is Promise<Awaited<T>>: await unwraps recursively, so Promise<T> would be a lie in that case.