TypeScript
TypeScript copied to clipboard
In-line chained .then to map an array causes weird behavior with the Map constructor
Bug Report
π Search Terms
map, map constructor, promise, then, await
π Version & Regression Information
- This changed between versions 4.5.5 and 4.6.2
β― Playground Link
Playground link with relevant code
π» Code
interface Data {
id: string;
}
async function fn(): Promise<Data[]> {
return [];
}
const map = new Map(
await fn().then((data) => data.map(d => [d.id, d]))
);
// for top-level-await
export {};
π Actual behavior
map
has a type of Map<unknown, unknown>
, incorrectly inferring Map<string, Data>
.
Interestingly enough, if we were to do
const intermediate = await fn().then((data) => data.map((d): [string, Data] => [d.id, d]));
const map = new Map(intermediate);
the type is inferred correctly. It should also be noted that this issue also doesn't occur when we're not dealing with promises, for example:
function fn(): Data[] {
return [];
}
const map = new Map(fn().map(d => [d.id, d]);
also works as intended
π Expected behavior
map
's type is correctly inferred to Map<string, Data>
The method signature for then
is
then<TResult1 = T, TResult2 = never>(
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
): Promise<TResult1 | TResult2>;
Whatβs happening is the contextual type Iterable<readonly [K, V]> | null
from the Map constructor parameter is being used as an inference source for the awaited return type TResult1 | TResult2
, so in the end TResult2
gets inferred as Iterable<readonly [unknown, unknown]>
which wipes out the correctly inferred result for TResult1
in subtype reduction π
This issue comes up outside map constructor, it can bite you whenever you use .then(onresolve)
when the return type is known.
let base: string | undefined
const a = base ?? await Promise.resolve("str").then(val => val)
// Why is this an error - because .then inferred it's return type based on the context
a satisfies string
// See, no error without .then.
const b = base ?? await Promise.resolve("str")
b satisfies string
https://www.typescriptlang.org/play?#code/DYUwLgBARghgziAXBOYBOBLAdgcwgHwgFcsATEAM2xFIFgAoB0SGCAXmnhAgH4eIYAdxgZIABTQB7ALYYEAOjQg4k4ADcQACgBEqNNoCU8sAAsQWTWpjB2APghXgBpuGjtOCXvyEjxU2QpKKupauuiGDAyscDBgclTKKOjYOAxQKLHxGIl6KZH0IAAeAA6SaJAA3gC+QA