Closed
Description
Bug Report
π Search Terms
v4.9 conditionals unlisted property narrowing in operator #50666
π Version & Regression Information
v4.9+, including nightly
working in v4.8.4
- This changed between versions 4.8.4 and 4.9
β― Playground Link
Playground link with relevant code
π» Code
type HandledResult = {data: any, errors?: any}
type UnhandledResult = {message: string}
const result = null as any as HandledResult | UnhandledResult
const noWork = (): HandledResult => {
if ('data' in result || 'errors' in result) return result // when checking an optional property second, I get an error
return {data: null, errors: [result.message]}
}
const work = (): HandledResult => {
if ('errors' in result || 'data' in result ) return result // reversing the order fixes the problem
return {data: null, errors: [result.message]}
}
π Actual behavior
In the noWork
function, typescript could not narrow down the type to being HandledResult
. When the or clause is switched, it works. Since Boolean(A || B) === Boolean(B || A)
this seems like an error.
π Expected behavior
The ordering of the conditionals should not impact the narrowing of the type.
Metadata
Metadata
Assignees
Type
Projects
Milestone
Relationships
Development
No branches or pull requests
Activity
RyanCavanaugh commentedon Feb 17, 2023
This code is confusing to reason about because it is written in a way that presupposes an invariant violation occurring somewhere along the way.
Our logic effectively thinks of it this way:
In "noWork", it goes like this:
"data" in result
, then it's clearly aHandledResult
-> OK"data" in result
, then it's clearly not aHandledResult
, since that's a mandatory property, so it must be anUnhandledResult
errors
is present, then it's must be aUnhandledResult & { errors: unknown }
; that's the only thing it can be. That's not a valid return type, so the program is correctly rejectedIn "work", it goes like this:
"errors" in result
then it's clearly aHandledResult
, since that's one of its listed optional properties"data" in result
then it's clearly aHandledResult
, since that's one of its listed required propertiesThe functions don't make any sense if you assume the types are closed, which is the only operative condition in which using
in
makes any sense in the first place. I don't really see a way to change this without breaking more reasonable code that would expectnot "data" in result
to produce anUnhandledResult
fatcerberus commentedon Feb 18, 2023
It's interesting to me that in the "working" case, TS never considers the possibility that
"data" in result
might be false, like it does in the "non-working" case. It seems like it should, since||
is commutative (short-circuiting of side effects notwithstanding).mattkrick commentedon Feb 21, 2023
thank you so much for your thoughtful explanation. π
it took a few re-reads, but it eventually makes sense why it works the way it does! I was treating them like open types where
{data?: any, errors: any}
could possibly exist & it prevented that.