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
Unsound handling of unions with multiple type variables (real this time) #49432
functionextract<K>(x: Array<K>|Array<Array<K>>): K{constarr=x[0];if(Array.isArray(arr)){returnarr[0];}returnarr;}functionextract2<T>(x: Array<T>): T{returnextract(x);}constx=extract2([[3]]);// hover to see type: `number[]`console.log(x);// but value is `3`x.map(x=>x);// which leads to runtime error: 'x.map is not a function'
🙁 Actual behavior
Runtime error not caught by TypeScript, due to incorrect inference.
Looks like a duplicate of #45686. This sort of filtering is very useful but unsound, but without negated types there's not really much better we can do. If you care strictly about safety then the call signature <T>(x: T[] | T[][])=>T cannot be implemented, because nothing stops someone from writing both of these:
Joe Calzaretta (@jcalz) thanks for taking a look, but I think I disagree on the diagnosis and am more hopeful the problem is solvable
If you care strictly about safety then the call signature <T>(x: T[] | T[][])=>T cannot be implemented
I agree such signatures (where there are overlapping unions with type variables) are probably mistakes, but they do occur in the wild.
I think it's main the job of a type checker to error on unsound code, not to assume that what the user wrote is intentional.
... without negated types there's not really much better we can do
Here are some options:
Flow approach playground, where the implementation of such a whacky function is rejected. Imo, it would be even better if the signature could also be rejected, at least in the common cases.
Typed Racket, iuc, accepts the definition by doing multiple rounds of constraint solving gist. I wasn't able to trick it like TypeScript is tricked here.
I see how the issue may have a common cause with #45686, but will leave this open for a bit because a different solution might be available. Thanks for the triage Joe Calzaretta (@jcalz)
I think it's main the job of a type checker to error on unsound code, not to assume that what the user wrote is intentional.
Welcome to the TypeScript Unsoundness Support Group! I'm Joe Calzaretta (@jcalz) and I have been a member for four years. Please enjoy the complimentary coffee; the cups leak a little bit so don't hold them directly over your lap. Today we'll be discussing TypeScript Language Design Non-Goal #3 and how it can be used as a mantra to stay calm in the face of logical contradictions.
I don't consider Flow's handling of this to be sound either. Example that checks in both checkers:
typeCat<T>={name: T,meow: string}typeMortgage<T>={length: T,amount: number};declarefunctionfn<T>(x: Cat<T>|Mortgage<T>): T;// if isCat(x) return x.name else return x.amountconstm: Cat<"whiskers">&Mortgage<30>={amount: 0,length: 30,meow: "",name: "whiskers"};constn: Mortgage<30>=m;functionfn(x: Mortgage<30>){consto: number=fn(x);// allowed but is "whiskers"}
There's not much we can do here that's useful. It's not plausible to warn on this kind of signature -- for one, it's effectively the same signature as Array#flat! Aliasing is always going to be capable of inducing incorrect inference results if the implementation isn't extremely watertight, but functions that are a tiny bit leaky are very common.
Switching to a sort of whole-program unification like Racket is clearly out of scope.
Switching to a sort of whole-program unification like Racket is clearly out of scope.
Typed Racket uses Local Type Inference, just like TypeScript
Section 4.3 of "Occurrence Typing Modulo Theories" https://arxiv.org/pdf/1511.07033.pdf. No whole-program inference.
I don't consider Flow's handling of this to be sound either. Example that checks in both checkers:
Flow's approach seems to be such that one can't define the "bad" functions. At least, I haven't managed to trick it yet:
functionbadUnion<K>(x: Array<K>|Array<Array<K>>): K{if(Array.isArray(x)){returnx[0];// Error in the body of the function. Your example had no body}returnx;}
I don't understand the alternative example you provided, which omits the definition but has a comment that seems to refer to type predicates. Type predicates are a TypeScript feature that Flow doesn't have: facebook/flow#34.
There's not much we can do here that's useful.
Whether TS should provide a diagnostic for the body of extract/badUnion is independent of whether there should be a diagnostic at the use site.
Re providing a diagnostic on the signature vs. the body of the "bad" function: fwiw I would expect a diagnostic somewhere for the code in the original example (like Flow and Typed Racket do).
It's not plausible to warn on this kind of signature -- for one, it's effectively the same signature as Array#flat
Warning on the signature is not strictly necessary, if there is a diagnostic for the body.
Was this issue closed by mistake?
It was marked "working as intended", but the main reason given seems to be that it is impossible to handle the code soundly with local type inference, which does not seem to be correct.
I replied above with more information about how similar type checkers solve the problem.
If it's deemed not worth it to fix the problem, could we change to #wontfix?
Won't Fix would be that we identified it as a defect, but decided fixing it wasn't worth the cost. That isn't the case here.
Here, we think this is the "right" behavior. The actual prevalence of the sort of problematic aliasing is not common enough to justify issuing many false positive warnings.
The TS+Typed-Racket-inspired type checker I worked on at WhatsApp, eqWAlizer, was released this week. Thought it might be helpful to share the behavior, which was simple to implement safely but required fine-tuning for decent error messages:
error: expected_subtype
┌─ wa_util/src/sample.erl:5:19
│
5 │ extract([[T]]) -> T;
│ ^ T.
Expected: T
Got : any()
See https://fburl.com/eqwalizer_expected_subtype
error: expected_subtype
┌─ wa_util/src/sample.erl:6:17
│
6 │ extract([T]) -> T.
│ ^
│ │
│ T.
Expected: T
Got : T | [T]
See https://fburl.com/eqwalizer_expected_subtype
│
T | [T] is not a subtype of T
because
[T] is not a subtype of T
error: expected_subtype
┌─ wa_util/src/sample.erl:9:26
│
9 │ extract2([T]) -> extract(T).
│ ^
│ │
│ T.
Expected: [T] | [[T]]
Got : T
See https://fburl.com/eqwalizer_expected_subtype
│
T is not a subtype of [T] | [[T]]
because
T is not a subtype of [T]
3 ERRORS
eqWAlizer's handling is not perfect: error messages could pinpoint the problem more precisely, especially for easy cases. But at least it seems to be sound and the implementation of generics+unions is roughly 20 LOC.
Activity
fatcerberus commentedon Jun 8, 2022
If this is the same bug, why didn’t you just post the corrected repro in the original issue?
edit: Disregard. Just noticed the original issue was closed as fixed so it's probably not an accurate description of the problem.
jcalz commentedon Jun 8, 2022
Looks like a duplicate of #45686. This sort of filtering is very useful but unsound, but without negated types there's not really much better we can do. If you care strictly about safety then the call signature
<T>(x: T[] | T[][])=>Tcannot be implemented, because nothing stops someone from writing both of these:but of course at runtime those are the same call. If you want to work around it in your own code you can try to be more explicit like
Playground link
mheiber commentedon Jun 9, 2022
Joe Calzaretta (@jcalz) thanks for taking a look, but I think I disagree on the diagnosis and am more hopeful the problem is solvable
I agree such signatures (where there are overlapping unions with type variables) are probably mistakes, but they do occur in the wild.
I think it's main the job of a type checker to error on unsound code, not to assume that what the user wrote is intentional.
Here are some options:
mheiber commentedon Jun 9, 2022
I see how the issue may have a common cause with #45686, but will leave this open for a bit because a different solution might be available. Thanks for the triage Joe Calzaretta (@jcalz)
jcalz commentedon Jun 9, 2022
Welcome to the TypeScript Unsoundness Support Group! I'm Joe Calzaretta (@jcalz) and I have been a member for four years. Please enjoy the complimentary coffee; the cups leak a little bit so don't hold them directly over your lap. Today we'll be discussing TypeScript Language Design Non-Goal #3 and how it can be used as a mantra to stay calm in the face of logical contradictions.
RyanCavanaugh commentedon Jun 9, 2022
I don't consider Flow's handling of this to be sound either. Example that checks in both checkers:
There's not much we can do here that's useful. It's not plausible to warn on this kind of signature -- for one, it's effectively the same signature as
Array#flat! Aliasing is always going to be capable of inducing incorrect inference results if the implementation isn't extremely watertight, but functions that are a tiny bit leaky are very common.Switching to a sort of whole-program unification like Racket is clearly out of scope.
mheiber commentedon Jun 9, 2022
Typed Racket uses Local Type Inference, just like TypeScript
Section 4.3 of "Occurrence Typing Modulo Theories" https://arxiv.org/pdf/1511.07033.pdf. No whole-program inference.
Flow's approach seems to be such that one can't define the "bad" functions. At least, I haven't managed to trick it yet:
playground link
I don't understand the alternative example you provided, which omits the definition but has a comment that seems to refer to type predicates. Type predicates are a TypeScript feature that Flow doesn't have: facebook/flow#34.
Whether TS should provide a diagnostic for the body of
extract/badUnionis independent of whether there should be a diagnostic at the use site.Re providing a diagnostic on the signature vs. the body of the "bad" function: fwiw I would expect a diagnostic somewhere for the code in the original example (like Flow and Typed Racket do).
Warning on the signature is not strictly necessary, if there is a diagnostic for the body.
But
Array#flatmay not be the best example, anyway, the signature doesn't use unions-with-type variables: https://github.com/microsoft/TypeScript/blob/35c1ba6/lib/lib.es2019.array.d.ts#L52. The issue I opened seems to be about unions-with-type-variables.typescript-bot commentedon Jun 11, 2022
This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.
mheiber commentedon Jun 12, 2022
Was this issue closed by mistake?
It was marked "working as intended", but the main reason given seems to be that it is impossible to handle the code soundly with local type inference, which does not seem to be correct.
I replied above with more information about how similar type checkers solve the problem.
If it's deemed not worth it to fix the problem, could we change to #wontfix?
RyanCavanaugh commentedon Jun 15, 2022
Won't Fix would be that we identified it as a defect, but decided fixing it wasn't worth the cost. That isn't the case here.
Here, we think this is the "right" behavior. The actual prevalence of the sort of problematic aliasing is not common enough to justify issuing many false positive warnings.
mheiber commentedon Aug 2, 2022
The TS+Typed-Racket-inspired type checker I worked on at WhatsApp, eqWAlizer, was released this week. Thought it might be helpful to share the behavior, which was simple to implement safely but required fine-tuning for decent error messages:
Given input:
We report these errors:
eqWAlizer's handling is not perfect: error messages could pinpoint the problem more precisely, especially for easy cases. But at least it seems to be sound and the implementation of generics+unions is roughly 20 LOC.
mheiber commentedon Jul 10, 2024
A sound implementation of the check in < LOC, and the only cases that are rejected are cases where the user wrote something ambiguous: https://github.com/WhatsApp/eqwalizer/blob/1dbdbbd/eqwalizer/src/main/scala/com/whatsapp/eqwalizer/tc/generics/Constraints.scala#L163