You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Possible Regression in v4.9: Conditionals using Unlisted Property Narrowing with the in Operator Β #52812
typeHandledResult={data: any,errors?: any}typeUnhandledResult={message: string}constresult=nullasanyasHandledResult|UnhandledResultconstnoWork=(): HandledResult=>{if('data'inresult||'errors'inresult)returnresult// when checking an optional property second, I get an error return{data: null,errors: [result.message]}}constwork=(): HandledResult=>{if('errors'inresult||'data'inresult)returnresult// reversing the order fixes the problemreturn{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.
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:
If "data" in result, then it's clearly a HandledResult -> OK
If not "data" in result, then it's clearly not a HandledResult, since that's a mandatory property, so it must be an UnhandledResult
If errors is present, then it's must be a UnhandledResult & { errors: unknown }; that's the only thing it can be. That's not a valid return type, so the program is correctly rejected
In "work", it goes like this:
If "errors" in result then it's clearly a HandledResult, since that's one of its listed optional properties
If "data" in result then it's clearly a HandledResult, since that's one of its listed required properties
Either way this code is fine
The 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 expect not "data" in result to produce an UnhandledResult
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).
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.
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 anUnhandledResulterrorsis 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 resultthen it's clearly aHandledResult, since that's one of its listed optional properties"data" in resultthen 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
inmakes 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 resultto produce anUnhandledResultfatcerberus commentedon Feb 18, 2023
It's interesting to me that in the "working" case, TS never considers the possibility that
"data" in resultmight 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.