Skip to content

3.7.2 Promise.all infers base type of interfaces rather than actual interface #34937

Description

@thaoula

TypeScript 3.7.2
Playground link

Compiler Options:

{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "strictBindCallApply": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "useDefineForClassFields": false,
    "alwaysStrict": true,
    "allowUnreachableCode": false,
    "allowUnusedLabels": false,
    "downlevelIteration": false,
    "noEmitHelpers": false,
    "noLib": false,
    "noStrictGenericChecks": false,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "esModuleInterop": true,
    "preserveConstEnums": false,
    "removeComments": false,
    "skipLibCheck": false,
    "checkJs": false,
    "allowJs": false,
    "experimentalDecorators": false,
    "emitDecoratorMetadata": false,
    "target": "ES2017",
    "module": "ESNext"
  }
}

Input:

interface QuoteFilter {
    _id: string;
}

interface QuoteFilterAdvanced extends QuoteFilter {
    name: string;
}

function getQuoteFilters(): Promise<QuoteFilter[]> {
    return new Promise<QuoteFilter[]>(
        (resolve, reject) => {
            resolve([
               { _id: 'Test' }
            ]);
        });
}

function getQuoteFiltersAdvanced(): Promise<QuoteFilterAdvanced[]> {
    return new Promise<QuoteFilterAdvanced[]>(
        (resolve, reject) => {
            resolve([
               { _id: 'Test', name: 'Test' }
            ]);
        });
}

async() => {
    const [filters, advanced] = await Promise.all([
        getQuoteFilters(),
        getQuoteFiltersAdvanced()
    ]);
}

Output:

"use strict";
function getQuoteFilters() {
    return new Promise((resolve, reject) => {
        resolve([
            { _id: 'Test' }
        ]);
    });
}
function getQuoteFiltersAdvanced() {
    return new Promise((resolve, reject) => {
        resolve([
            { _id: 'Test', name: 'Test' }
        ]);
    });
}
async () => {
    const [filters, advanced] = await Promise.all([
        getQuoteFilters(),
        getQuoteFiltersAdvanced()
    ]);
};

Expected behavior:
I am expecting the advanced variable to be an array of QuoteFilterAdvanced rather than QuoteFilter.

This was correctly inferred in Typescript 3.6.3

Activity

  1. AnyhowStep commented on Nov 6, 2019

    @AnyhowStep
    Contributor
    type Awaited<T> =
        T extends undefined ?
        T :
        T extends PromiseLike<infer U> ?
        U :
        T
    ;
    declare function all<T extends readonly any[]>(
        values: T
    ): Promise<{ -readonly [P in keyof T]: Awaited<T[P]> }>;
    
    //for empty tuple
    declare function betterAll (arr : []) : Promise<[]>;
    //for non-empty tuple
    declare function betterAll<ArrT extends readonly [any, ...any[]]>(
        arr: ArrT
    ): Promise<{ -readonly [i in keyof ArrT]: Awaited<ArrT[i]> }>;
    //for non-tuple array
    declare function betterAll<ArrT extends readonly any[]>(
        arr: ArrT
    ): Promise<{ -readonly [i in keyof ArrT]: Awaited<ArrT[i]> }>;
    
    //https:https://github.com/microsoft/TypeScript/issues/34937
    interface QuoteFilter {
        _id: string;
    }
    
    interface QuoteFilterAdvanced extends QuoteFilter {
        name: string;
    }
    
    function getQuoteFilters(): Promise<QuoteFilter[]> {
        return new Promise<QuoteFilter[]>(
            (resolve, reject) => {
                resolve([
                   { _id: 'Test' }
                ]);
            });
    }
    
    function getQuoteFiltersAdvanced(): Promise<QuoteFilterAdvanced[]> {
        return new Promise<QuoteFilterAdvanced[]>(
            (resolve, reject) => {
                resolve([
                   { _id: 'Test', name: 'Test' }
                ]);
            });
    }
    
    async () => {
        //const filters: QuoteFilter[]
        //const advanced: QuoteFilter[]
        const [filters, advanced] = await Promise.all([
            getQuoteFilters(),
            getQuoteFiltersAdvanced()
        ]);
    }
    
    async () => {
        //const filters: QuoteFilter[]
        //const advanced: QuoteFilterAdvanced[]
        const [filters, advanced] = await all([
            getQuoteFilters(),
            getQuoteFiltersAdvanced()
        ]);
    }
    
    async () => {
        //const filters: QuoteFilter[]
        //const advanced: QuoteFilterAdvanced[]
        const [filters, advanced] = await betterAll([
            getQuoteFilters(),
            getQuoteFiltersAdvanced()
        ]);
    }

    Playground

    See #33707

  2. axross commented on Nov 6, 2019

    @axross

    Same here. This could look a bug

  3. BooleT37 commented on Nov 6, 2019

    @BooleT37

    The same issue occurs even if you don't explicitly inherit types: playground, which looks very odd.
    Try to rename foo: string; to anything else (i.e.foz: string;) and the code compiles.

  4. XGHeaven commented on Nov 7, 2019

    @XGHeaven

    This looks like a bug. I found Promise.all use wrong overload on the above example.
    It use definition in lib.es2015.iterable.d.ts not lib.es2015.promise.d.ts.
    But in old version, for example 3.6.2, It use correct definition in lib.es2015.promise.d.ts

  5. aleksandar-kandzhichki commented on Nov 7, 2019

    @aleksandar-kandzhichki

    Same here, it does not infer the type properly
    image

  6. jablko commented on Nov 7, 2019

    @jablko
    Contributor

    XGHeaven I believe the incorrect results you are getting are indeed from the overload in es2015.promise.d.ts (not the one in es2015.iterable.d.ts). In my tests, if I drop all but the overload from es2015.promise.d.ts, I continue to get the same results. FYI.

  7. XGHeaven commented on Nov 8, 2019

    @XGHeaven

    Jack Bates (@jablko)
    I can give you an example. In 3.7.2, It use es2015.iterable.d.ts. This is wrong. Because The type of first and second is number | string.
    image
    Same code in 3.6.4. It use es2015.promise.d.ts. This is correct. The first type is number and the second type is string.
    image
    So the correct results should be come from es2015.promise.d.ts.

  8. chadbr commented on Nov 8, 2019

    @chadbr

    seeing same issue here... undefined is added to everything I call in a Promise.all...

  9. changed the title [-]3.7.2 Promise.all does infers base type of interfaces rather than actual interface[/-] [+]3.7.2 Promise.all infers base type of interfaces rather than actual interface[/+] on Nov 10, 2019
  10. jablko commented on Nov 11, 2019

    @jablko
    Contributor

    XGHeaven I see what you mean, however I think you've highlighted a separate issue with the playground: That example doesn't work in the playground regardless of version (3.7.2 or 3.6.3). https://www.typescriptlang.org/play/index.html?ts=3.6.3#code/IYZwngdgxgBAZgV2gFwJYHsIwOYFNkCCANkQJLK4C2IAFAJQwDeAUDGzFJiMjANpyoATtwA0MELk4QAJgF0AXHwgJKAI1yCx3QagjZZMALwxgAd2CoeABUHpKqCQDpgJGrwCMYgORfZdANzMAL5AA

    I think it's a symptom of microsoft/TypeScript-Website#82 (the playground doesn't use the lib d.ts files that get shipped with a build).

    In my tests (locally) it doesn't use es2015.iterable.d.ts, in 3.6 or 3.7. (I do get the same incorrect results as you in 3.7, but they're not coming from es2015.iterable.d.ts.)

  11. Manc commented on Nov 12, 2019

    @Manc

    I found something! Is it possible that the added readonly in version 3.7.2 messes things up? I did an experiment…

    In my case, all types from all Promises are mashed together in TypeScript 3.7.2, not in 3.6.3. As far as I can tell by looking at Promise.all in Visual Code Studio, both versions point to lib.es2015.promise.d.ts. In my example with three Promises, that's…

    In 3.7.2:

    all<T1, T2, T3>(values: readonly [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>]): Promise<[T1, T2, T3]>;
    

    In 3.6.3:

    all<T1, T2, T3>(values: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>]): Promise<[T1, T2, T3]>;
    

    The only difference there is the added readonly in 3.7.2. So I commented out the above line from version 3.7.2 and replaced it with the line from 3.6.3 and the error went away!

    This was changed here: 29becf0#diff-ca94294a2cb9f29f16a30f5d32e56c2e

  12. jablko commented on Nov 12, 2019

    @jablko
    Contributor
  13. shayded-exe commented on Nov 14, 2019

    @shayded-exe

    A workaround for this is to use as const when passing the array to Promise.all.

  14. deftomat commented on Dec 10, 2019

    @deftomat

    Similar issue.

    This works in 3.6.3 and fails in 3.7 and 3.8.0-dev.20191210

    async function foo(): Promise<number> {
      const [first, second] = await Promise.all([generateNumber(), generateStringOrNull()]);
      return first; // ERROR: Type 'number | null' is not assignable to type 'number'.
    }
    
    async function generateNumber(): Promise<number> {
      return 10;
    }
    
    async function generateStringOrNull(): Promise<string| null> {
      return '10';
    }

    first should be number not a number | null.

    I can confirm that workaround works for me.

  15. swftvsn commented on Dec 13, 2019

    @swftvsn

    We've also hit with this when using 3.7.3. I can too confirm that the as const workaround works for me too.

  16. locked as resolved and limited conversation to collaborators on Oct 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugA bug in TypeScriptFix AvailableA PR has been opened for this issue

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions