Skip to content

Narrow subtype-reduction-prone unions to their narrowest constituent #47731

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

Open
paul-marechal opened this issue Feb 3, 2022 · 6 comments
Open
Labels
Bug A bug in TypeScript Help Wanted You can do this
Milestone

Comments

@paul-marechal
Copy link

Bug Report

Type narrowing doesn't seem to take into account an assignment I made to ensure the variable has the right type.

Maybe related? #43584 (comment)

🔎 Search Terms

type narrowing assignment

🕗 Version & Regression Information

4.5.4

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about "type narrowing"

⏯ Playground Link

Playground Link

💻 Code

declare let a: object | any[] | undefined

if (a === undefined) {
    a = [] // I expect this line to have TS understand that a is any[] from now on
} else if (!Array.isArray(a)) {
    throw new Error()
}
[...a] // complains here because a is object | any[]

🙁 Actual behavior

a has type object | any[]

🙂 Expected behavior

a to have type any[]

@fatcerberus
Copy link

This is expected; arrays are assignable to object.

@paul-marechal
Copy link
Author

@fatcerberus while it might be the case, I don't see how that makes it any less unexpected?

Or am I wrong to expect that since I explicitly pass an array the typings should narrow to that?

I tried a = [] as const or a = [] as any[] and still get the same issue, it really feels counter-intuitive.

@fatcerberus
Copy link

[] is both a valid any[] and a valid object. Therefore TS can't narrow away the object. You're mistakenly assuming object means "any non-array object" but it actually means "any object at all". In fact, because object is a supertype of any[], the compiler is well within its rights to completely forget about the any[] part of the union, which it will do in certain cases (called subtype reduction):

image

In general, having overlapping types in a union is going to give you a bad time.

@RyanCavanaugh
Copy link
Member

You could write

declare let a: { push?: never } | any[] | undefined

if (a === undefined) {
    a = [] // I expect this line to have TS understand that a is any[] from now on
} else if (!Array.isArray(a)) {
    throw new Error()
}

Subtype-reducing unions aren't great but it seems like we could fix the narrowing-on-assignment logic to do a subtype reduction on any resulting union, since that's assured to be sound.

@RyanCavanaugh RyanCavanaugh added Bug A bug in TypeScript Help Wanted You can do this labels Feb 4, 2022
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Feb 4, 2022
@RyanCavanaugh RyanCavanaugh changed the title Type narrowing doesn't work properly following assignment Narrow subtype-reduction-prone unions to their narrowest constituent on assignment Feb 4, 2022
@RyanCavanaugh RyanCavanaugh changed the title Narrow subtype-reduction-prone unions to their narrowest constituent on assignment Narrow subtype-reduction-prone unions to their narrowest constituent Feb 4, 2022
@paul-marechal
Copy link
Author

paul-marechal commented Feb 4, 2022

You're mistakenly assuming object means "any non-array object" but it actually means "any object at all".

@fatcerberus I very much understand this, hence why we can do the following:

const b: any[] = []
const c: object = b

What I think isn't ergonomic is not narrowing the type to what's being assigned explicitly:

let d: object | any[] | undefined
d = []
takeArray(d) // should work, currently doesn't

But @RyanCavanaugh's suggestion works!

let d: { push?: never } | any[] | undefined
d = []
takeArray(d) // works

@fatcerberus thank you for explaining how TypeScript currently sees this scenario but no matter what it does, the point of my issue is that maybe there would be something to change so that it is more ergonomical.

@fatcerberus
Copy link

the point of my issue is that maybe there would be something to change so that it is more ergonomical

Indeed, and I agree this is less than ergonomic. Note @RyanCavanaugh's renaming of the issue which is one way this could be solved. It's just that you posted this as a bug report while the observed behavior actually falls out naturally from the rules of the type system, so it seemed like a misunderstanding that I was trying to clear up (because fixing this adds yet another ad-hoc rule to type inference/narrowing that's not based purely on assignability, and IMO type inference in TS is way too ad-hoc as it is). Apparently Ryan agrees with you that it's a bug though so... yeah 😛

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Help Wanted You can do this
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants