Skip to content

Assignment of nested field accessed from union type indexed field expects union type instead of intersection type #50249

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
mgmolisani opened this issue Aug 10, 2022 · 4 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@mgmolisani
Copy link

Bug Report

🔎 Search Terms

assignment union intersection nested get set

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about field assignment versus access types specifically in the context of shared nested fields.

⏯ Playground Link

Playground link with relevant code

💻 Code

/* Both the x and y fields share the type:

type Foo<T> = {
    nested: T
}

*/
const foo = { x: { nested: { a: 1 } }, y: { nested: { b: 2 } } }
const key = Math.random() > 0.5 ? `x` : `y`
const bar1 = foo[key] // correct - union, the type must be one OR the other
const bar2 = foo[key].nested // correct - union, the type must be one OR the other
foo[key] = { nested: { a: 1, b: 1 } } // correct - intersection, the type must be assignable to both types simultaneously
foo[key].nested = { a: 1 } // incorrect - union, { x: number } should not be assignable if key === `y` but this is allowed

🙁 Actual behavior

When accessing a field via any valid key that is unknown, the compiler correctly asserts that the type returned is the union of the possible types at the possible keys.

When assigning a field via any valid key that is unknown, the compiler correctly asserts that the type to be set must be the intersection of all the possible types. However, if you access a shared nested field and attempt to assign it, the expected assignment is a union type. This seems incorrect as the assignment again should be the intersection of the possible types that the shared nested property could be across all valid keys used to access the parent property.

🙂 Expected behavior

Discussed above, but again, regardless of if the field or its parent were accessed via an unknown but valid key, the expected assignment type should be an intersection, even if the intersection is never, as that would be the only valid way to ensure any property assigned has been given a valid value.

@RyanCavanaugh RyanCavanaugh added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Aug 10, 2022
@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Aug 10, 2022

It's clearer what's going on here if you separate out the intermediate expression:

const m = foo[key];
m.nested = { a: 1 };

The type of m is { nested: { a: number} | { b: number } }. To manifest any other behavior than what you see today, m would have to have some other type -- but what type? There isn't any type that has the same behavior as what's needed to replicate what's going on in e[k] = v (an expression form which is special-cased to improve soundness over the default covariant behavior), nor is it clear what that kind of type would even be.

@mgmolisani
Copy link
Author

mgmolisani commented Aug 10, 2022

@RyanCavanaugh appreciate the clarification

So just to make sure I'm understanding (as this was also my assumption) - because the property is accessed first, we get the type as it was accessed and lose the ability to treat it like the special-case expression type that changes the type based on the presence of the assignment operator?

A follow up: is there any simple/obvious way to simulate that expression special case type (with or without runtime implication ideally without casting) to at least allow for a better programming interface to avoid gotchas on the end user side of the API? Some way to capture the intended type?

@RyanCavanaugh
Copy link
Member

Correct

Functions types will unify a bit better; demo speaks louder than words

const foo = { x: { nested: { a: 1 }, setNested(_a: {a: number}) { } }, y: { nested: { b: 2 }, setNested(_b: { b: number }) {} } }
// ...
const m = foo[key];
m.setNested({ a: 1 }) // Missing 'b'

@KotlinIsland
Copy link

KotlinIsland commented Aug 21, 2022

Is this the same issue in #50384?

declare let a: { t: number } | { t: string }
a.t = "a string?"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

3 participants