Skip to content

return type of function returning a promise or a an empty object is incorrectly inferred unless promise is awaited #54524

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
jacekkarczmarczyk opened this issue Jun 5, 2023 · 7 comments
Labels
Not a Defect This behavior is one of several equally-correct options

Comments

@jacekkarczmarczyk
Copy link

Bug Report

πŸ”Ž Search Terms

function, return type, await, empty object

πŸ•— Version & Regression Information

  • This changed between versions 4.9 and 5.0

Nothing in the TS 5.0 release notes seems to be related to this change of the behaviour

⏯ Playground Link

πŸ’» Code

declare function getData(): Promise<{ [key in string]: unknown }>;

const fn = async (test: boolean) => test ? getData() : {}
//    ^? const fn (test: boolean) => Promise<{ [key in string]: unknown }>    in TS 4.9
//    ^? const fn (test: boolean) => Promise<{}>     in TS 5.0

const fnAwaited = async (test: boolean) => test ? await getData() : {}
//    ^? const fnAwaited (test: boolean) => Promise<{ [key in string]: unknown }>    in TS 4.9, 5.0

playground, TS 4.9

playground, TS 5.0

πŸ™ Actual behavior

Function's return type is Promise<{}>

πŸ™‚ Expected behavior

Function's return type should be Promise<{ [key in string]: unknown }>

Here's the code that's more close to the actual use case:

declare function getData(): Promise<{ [key in string]: unknown }>;
declare function useAsyncComputed<R> (load: () => Promise<R>, defaultValue: R): R;
declare const test: boolean;

const foo = useAsyncComputed(async () => test ? getData() : {}, {});

console.log(foo.whatever);

playground, TS 4.9

playground, TS 5.0

This is a pattern I'm using in multiple places in my app with TS 4.9, and if I wanted to upgrde to TS 5.0 I'd need to add await before every getData() call, which I know I can do automatically using eslint, but I don't think that should be necessary

@MartinJohns
Copy link
Contributor

IMO this is as expected. The type {} is a supertype of both Promise<{ [key in string]: unknown }> and { [key in string]: unknown }, so subtype reduction kicks in and simplifies the type to {}. You can work around this by adding a type annotation for your return type.

@jacekkarczmarczyk
Copy link
Author

Why was that working in 4.9? It seems to be a quite big change and as I mentioned I haven't found anything in release notes that could be related to it, am i missing something?

@MartinJohns
Copy link
Contributor

As per Ryan wrote: #50171 (comment)

Subtype reduction isn't required to happen, but is allowed to happen at any point.

TypeScript makes no guarantees when subtype reduction happens, and it can change between versions. It's an internal detail of the compiler. Both results are equally valid, just one is undesirable in your case.

@jacekkarczmarczyk
Copy link
Author

I see, I guess nothing I can do about it

One question though - snippet with useAsyncComputed works fine for me when I add await before the function call - is it theoretically possible that at some point TS will change the behaviour same like it changed when there was no await? Adding await could be done automatically with eslint, but adding return type annotations manually would be a tedious task (I have this pattern used in almost 200 places)

@fatcerberus
Copy link

IIRC subtyping rules were changed somewhere around TS 5.0 for {} so that Record<string, T> is considered a strict subtype (they were previously mutual subtypes), so that may be the reason for the change in behavior here.

Note that { [key in string]: unknown } is longhand for Record<string, unknown>.

@jacekkarczmarczyk
Copy link
Author

I'm actually using { [key in string]?: SomeType } in my actual code (I find Record<string, ...> useless in most scenarios), but the outcome is the same

@RyanCavanaugh RyanCavanaugh added the Not a Defect This behavior is one of several equally-correct options label Jun 12, 2023
@typescript-bot
Copy link
Collaborator

This issue has been marked as 'Not a Defect' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Jun 15, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Not a Defect This behavior is one of several equally-correct options
Projects
None yet
Development

No branches or pull requests

5 participants