TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

In-line chained .then to map an array causes weird behavior with the Map constructor

Open didinele opened this issue 2 years ago β€’ 2 comments

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>

didinele avatar May 14 '22 14:05 didinele

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 πŸ˜–

andrewbranch avatar May 25 '22 17:05 andrewbranch

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

everett1992 avatar Jun 05 '23 23:06 everett1992