Skip to content
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

Entries cannot be reduced to an interface due to any being assigned to never #33651

Closed
ibarsi opened this issue Sep 28, 2019 · 4 comments
Closed

Comments

@ibarsi
Copy link

ibarsi commented Sep 28, 2019

TypeScript Version: 3.7.0-dev.20190928

Search Terms:

  • any assigned to never
  • reduce any to never
  • cannot reduce to interface

Code

Ever since my team has updated to v3.6.3, the introduction of never has been causing us headaches whenever we try to reduce an array into a single typed object. The following example illustrates the issue we're faced with.

interface Thing {
    foo: number,
    bar: string,
}

const list: Array<[keyof Thing, any]> = [['foo', 1], ['bar', 'baz']];

const result: Thing = list.reduce((obj: Thing, [key, value]) => {
    // Compile-time error is thrown here because `any` is being assigned to `never`.
    obj[key] = value;
    return obj;
}, { foo: 0, bar: ''});

My best guess is that this behaviour is due to recent changes in TS 3.5 to fix unsound writes to indexed access types. While understandable, I'm having some difficulty trying to reason about how we might achieve the behaviour demonstrated above without type aliasing obj as any or something similar? My gut tells me this should be achievable without breaking out of the type system (i.e. aliasing), which leads me to believe this could potentially be a bug.

Expected behavior:
value of type any is allowed to be set as a property of Thing.

Actual behavior:
The following compile-time error is thrown.

Playground Link:

https://www.typescriptlang.org/play/?target=99#code/JYOwLgpgTgZghgYwgAgCoAtQHNkG8BQyRyMA9qQFzIgCuAtgEbQA0hxDcUVAzmFNqwC++fAlIheyADbBeVAIJQocAJ4AeANoBrCCtIw0mEFmbI4IFQF0AfMgC8yDRoDkZUs9MBGS6ZccoHsjOHABezpaWANwiYhJgyFAQ3DRSYFQY2PbSsmAAdIkAJjRIABQlpAwAVulGJo46KqYAbnBSNBCWAJT2tgTEyBWV2rqWWS1tENH9iWA0UCADVdGCprgk5FQADKb+VM7Ogp2RQA

Related Issues:
N/A

@jack-williams
Copy link
Collaborator

This is a consequence of #30769 combined with #31838: the former mandates that value must be assignable to the intersection of the properties and the later reduces the resulting intersection number & string to never. Everything is working as intended, though there are breaking changes involved.

Perhaps the cleanest way to resolve this without using casts is to introduce a generic parameter.

const result: Thing = list.reduce(<K extends keyof Thing>(obj: Thing, [key, value]: [K, any]) => {
    obj[key] = value;
    return obj;
}, { foo: 0, bar: ''});

The exploits the fact that the effects of #30769 do not apply to generic indexed access types.

@ibarsi
Copy link
Author

ibarsi commented Sep 28, 2019

First of all, thanks @jack-williams for the incredibly fast response ⚡️

I'm glad you've shown me a way to get around this without having to cast. I got curious and wanted to understand why this workaround exists, which caused me to start going down a rabbit hole of GitHub comments starting with yours back in April.

Does the gist of this loophole come down to the fact that if an index is generic then TS currently can't resolve where it lands on an interface, meaning the type of that key's associated value can't be determined so it defaults to any?

@jack-williams
Copy link
Collaborator

Does the gist of this loophole come down to the fact that if an index is generic then TS currently can't resolve where it lands on an interface, meaning the type of that key's associated value can't be determined so it defaults to any?

You are correct in saying that TS is unable to determine precisely where it lands, but it's not so permissive to default to any. For instance, this still gives an error:

obj[key] = true;

Roughly, in the original example (which is concrete) TS knows all the types and can 'simplify' the indexed access for writing:

Thing["foo" | "bar"]
--> Thing["foo"] & Thing["bar"]
--> number & string
--> never

In the modified example (which is generic), TS doesn't precisely know all the types and defers any kind of approximation of the property type, so we have something like Thing[K]. Once we have this deferred indexed access type the normals rules of any kick in and therefore value is assignable to obj[key].

It's worthing highlighting that never is the only type that any is not assignable to.

@ibarsi
Copy link
Author

ibarsi commented Sep 28, 2019

Got it, that makes sense and like you said #31838 is where never is coming from.

Thanks again for the detailed explanation and workaround 👍🏼 Closing the issue now as it's expected behaviour.

@ibarsi ibarsi closed this as completed Sep 28, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants