microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.26k stars 12.52k forks source link

Type inference is lose inside a promise #27001

Closed michaeljota closed 5 years ago

michaeljota commented 6 years ago

TypeScript Version: 3.1.0-dev.201xxxxx

Search Terms:

Code

interface ResponseA {
    type: 'A',
    payload: any,
}

interface ResponseB {
    type: 'B',
    payload: any,
}

type ResponseKind = ResponseA | ResponseB;

// Does not work in strict mode
async function returnResponseAsync(): Promise<ResponseKind> {
    const type = 'A';
    const payload = {};

    return await {
        type, // type is 'string' type
        payload
    }
}

// Works in strict mode
function returnResponse(): ResponseKind {
    const type = 'A';
    const payload = {};

    return {
        type, // type is 'A' type
        payload
    }
}

This is a minimum reproducible code but I found this working with a complex scenario of observables and promises. However, the bug is still reproducible, because the type is being lose.

Expected behavior:

Inside the promise type type should be inferred as well.

Actual behavior:

Throws because string is not assignable to neither A or B.

Playground Link:

[Link using async/await](http://www.typescriptlang.org/play/#src=interface%20ResponseA%20%7B%0D%0A%20%20%20%20type%3A%20'A'%2C%0D%0A%20%20%20%20payload%3A%20any%2C%0D%0A%7D%0D%0A%0D%0Ainterface%20ResponseB%20%7B%0D%0A%20%20%20%20type%3A%20'B'%2C%0D%0A%20%20%20%20payload%3A%20any%2C%0D%0A%7D%0D%0A%0D%0Atype%20ResponseKind%20%3D%20ResponseA%20%7C%20ResponseB%3B%0D%0A%0D%0A%2F%2F%20Does%20not%20work%20in%20strict%20mode%0D%0Aasync%20function%20returnResponseAsync()%3A%20Promise%3CResponseKind%3E%20%7B%0D%0A%20%20%20%20const%20type%20%3D%20'A'%3B%0D%0A%20%20%20%20const%20payload%20%3D%20%7B%7D%3B%0D%0A%0D%0A%20%20%20%20return%20await%20%7B%0D%0A%20%20%20%20%20%20%20%20type%2C%20%2F%2F%20type%20is%20'string'%20type%0D%0A%20%20%20%20%20%20%20%20payload%0D%0A%20%20%20%20%7D%0D%0A%7D%0D%0A%0D%0A%2F%2F%20Works%20in%20strict%20mode%0D%0Afunction%20returnResponse()%3A%20ResponseKind%20%7B%0D%0A%20%20%20%20const%20type%20%3D%20'A'%3B%0D%0A%20%20%20%20const%20payload%20%3D%20%7B%7D%3B%0D%0A%0D%0A%20%20%20%20return%20%7B%0D%0A%20%20%20%20%20%20%20%20type%2C%20%2F%2F%20type%20is%20'A'%20type%0D%0A%20%20%20%20%20%20%20%20payload%0D%0A%20%20%20%20%7D%0D%0A%7D)

[Link with Promise.resolve](http://www.typescriptlang.org/play/#src=interface%20ResponseA%20%7B%0D%0A%20%20%20%20type%3A%20'A'%2C%0D%0A%20%20%20%20payload%3A%20any%2C%0D%0A%7D%0D%0A%0D%0Ainterface%20ResponseB%20%7B%0D%0A%20%20%20%20type%3A%20'B'%2C%0D%0A%20%20%20%20payload%3A%20any%2C%0D%0A%7D%0D%0A%0D%0Atype%20ResponseKind%20%3D%20ResponseA%20%7C%20ResponseB%3B%0D%0A%0D%0A%2F%2F%20Does%20not%20work%20in%20strict%20mode%0D%0Afunction%20returnResponseAsync()%3A%20Promise%3CResponseKind%3E%20%7B%0D%0A%20%20%20%20const%20type%20%3D%20'A'%3B%0D%0A%20%20%20%20const%20payload%20%3D%20%7B%7D%3B%0D%0A%0D%0A%20%20%20%20return%20Promise.resolve(%7B%0D%0A%20%20%20%20%20%20%20%20type%2C%20%2F%2F%20type%20is%20'string'%20type%0D%0A%20%20%20%20%20%20%20%20payload%0D%0A%20%20%20%20%7D)%0D%0A%7D%0D%0A%0D%0A%2F%2F%20Works%20in%20strict%20mode%0D%0Afunction%20returnResponse()%3A%20ResponseKind%20%7B%0D%0A%20%20%20%20const%20type%20%3D%20'A'%3B%0D%0A%20%20%20%20const%20payload%20%3D%20%7B%7D%3B%0D%0A%0D%0A%20%20%20%20return%20%7B%0D%0A%20%20%20%20%20%20%20%20type%2C%20%2F%2F%20type%20is%20'A'%20type%0D%0A%20%20%20%20%20%20%20%20payload%0D%0A%20%20%20%20%7D%0D%0A%7D)

Link with Observable

Related Issues:

Workaround:

Just type the variable with a literal type.

async function returnResponseAsync(): Promise<ResponseKind> {
    const type: 'A' = 'A';
    const payload = {};

    return await {
        type, // type is 'A' type
        payload
    }
}
ajafff commented 6 years ago

Just remove the await from return await. It causes the returned object literal to not be contextually typed.

Also note: you shouldn't use return await unless you have one of the rare cases where it's really necessary

michaeljota commented 6 years ago

I mean that. As I said, I just wanted to recreate the escenario, but actually I'm not just return await. Just returing an object inside an promise, and then the type is lose.

michaeljota commented 6 years ago

@ajafff I update the issue with another reproducible scenario using Promise.resolve.

RyanCavanaugh commented 6 years ago

We should unwrap a Promise through an await expression to create a contextual type

Curious72 commented 6 years ago

Hey , can i work on this issue ?

DanielRosenwasser commented 6 years ago

Absolutely! I believe what you'll want to do is start at getContextualType in checker.ts. From there, you'll want to handle the case for AwaitExpression. In that case I think you'll want to retrieve the contextual type from the AwaitExpression itself. Let's call that T. You'll want to contextually type it as T | PromiseLike<T>

michaeljota commented 6 years ago

Update here: I notice that in my case I was using an observable, and then returning it as a promise, but even in the Observable the literal type of type was being loss.

I create an scenario that resemble more my use case, and may help to find the bug.

with Observable

Also, this would not be directly about Promises anymore, but maybe how Typescript pass literal types as arguments to generic types.

@DanielRosenwasser @RyanCavanaugh

michaeljota commented 6 years ago

@DanielRosenwasser Does the type resolves correctly in the Observable scenario after the PR? I'm afraid I miss explain the problem.

If you notice here the type of the response inside the tap pipe function is string, but should be One.

bgotink commented 6 years ago

Note you can fix the function type if you explicitly set the type of the return value, e.g.

const fn = () => 'one'; // return type: string
const fn = () => 'one' as 'one'; // return type: 'one'

function fn() {
  const val = 'one';
  return val; // return type: string
}
function fn() {
  const val = 'one';
  return val as typeof val; // return type: 'one'
}

but also, and this one is less obvious, the following:

function fn() {
  const val: 'one' = 'one';
  return val; // return type: 'one'
}

While I've run into this issue as well, I wonder whether it's a good idea to change the current behaviour. Most often, if a function returns a string literal, the developer wants the function's return type to be string. The same goes for number literals that get widened to number, boolean literals that get widened to boolean etc.

rbuckton commented 5 years ago

This was fixed by #27270.