Skip to content

Unexpected differences in distribution over union types: type string | 5 behaves differently than type string | [5] #61753

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
MMF2 opened this issue May 22, 2025 · 2 comments

Comments

@MMF2
Copy link

MMF2 commented May 22, 2025

🔎 Search Terms

"distribution union"

I have a type DistributeOverTupleElementTypes that is intended to operate on a tuple type and to distribute over the component union elements. Example:
DistributeOverTupleElementTypes<[number | boolean, string | 5]> is expected to evaluate to [number, string] | [number, 5] | [boolean, string] | [boolean, 5] and it works as expected (see t1 below). However, when replacing type "5" with type "[5]", the expected evaluation result misses the components [boolean, string] and [boolean, [5]] (see t2, t3, t4 and t5). Why? Is the behavioral difference intended?

type DistributeOverTupleElementTypes<T> =
    T extends [infer SingleComponent]
       ? (SingleComponent extends unknown // enforce distribution
          ? [SingleComponent]
          : never)
       : (T extends [infer First, ...infer Rest]
          ? (First extends unknown // enforce distribution
             ? (DistributeOverTupleElementTypes<Rest> extends infer EvaluatedRestU
                ? (EvaluatedRestU extends infer FixedEvaluatedRestTuple // enforce distribution
                   ? (FixedEvaluatedRestTuple extends unknown[]
                      ? [First, ...FixedEvaluatedRestTuple]
                      : never)
                   : never)
                : never)
             : never)
          : never);
const t1: [number, string] | [number, 5] | [boolean, string] | [boolean, 5] extends  DistributeOverTupleElementTypes<[number | boolean, string | 5]> ? true : false = true;
const t2: [number, string] | [number, [5]] | [boolean, string] | [boolean, [5]] extends  DistributeOverTupleElementTypes<[number | boolean, string | [5]]> ? true : false = true;  //!! error
const t3: [number, string] | [number, [5]] | [boolean, string] | [boolean, [5]] extends  DistributeOverTupleElementTypes<[number | boolean, string | [5]]> | [boolean, string] ? true : false = true; // !!error
const t4: [number, string] | [number, [5]] | [boolean, string] | [boolean, [5]] extends  DistributeOverTupleElementTypes<[number | boolean, string | [5]]> | [boolean, [5]] ? true : false = true; // !!error
const t5: [number, string] | [number, [5]] | [boolean, string] | [boolean, [5]] extends  DistributeOverTupleElementTypes<[number | boolean, string | [5]]> | [boolean, string] | [boolean, [5]] ? true : false = true;
// Thus, the evaluation result of DistributeOverTupleElementTypes<[number | boolean, string | [5]]> misses the union components [boolean, string] and [boolean, [5]]. Why?

const t6: [number, string | [5]] | [boolean, string | [5]] extends  DistributeOverTupleElementTypes<[number | boolean, string | 5]> ? true : false = true; // !!error
Output
"use strict";
const t1 = true;
const t2 = true; //!! error
const t3 = true; // !!error
const t4 = true; // !!error
const t5 = true;
// Thus, the evaluation result of DistributeOverTupleElementTypes<[number | boolean, string | [5]]> misses the union components [boolean, string] and [boolean, [5]]. Why?
const t6 = true; // !!error
Compiler Options
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "strictBindCallApply": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "alwaysStrict": true,
    "esModuleInterop": true,
    "declaration": true,
    "target": "ES2017",
    "jsx": "react",
    "module": "ESNext",
    "moduleResolution": "node"
  }
}

Playground Link: Provided

🕗 Version & Regression Information

  • This is the behavior in every version I tried starting with version 5.04 up to 5.8.3 and the Nightly version as of 2025-05-22.

⏯ Playground Link

https://www.typescriptlang.org/play/?#code/C4TwDgpgBAIglgZ2AJzgIwK7AgeQG4TIAqGYANhAKIUC2EAdsEeBAgDxEB8UAvAFBRBUIlAgAPbPQAmCKAG049AGaEoAZUUBzCgGEA9jTB76DYAF0BQoQH4oACg31tEfYeOnREhjKgZ6Aa3o9AHd6KAB6cNFlPWQAY2gpRBR0LDhjSysrWzlHZ1cjE0YLLKyALigTAmQASkyrCrsRcUkfBWVVADE4ZCQAGigAOmHFFWQoACVWc3rS2ztu3uBPVtk-QJCwyOilWISoJKRUTGB0+lnSwXn4I9TsfEIScipaU2ZIdimkbhbvWVHVJQ8ABDMgYYHYKRfYAAVQulxs9iBoPBkOhMJWfygAPG3TEECkyLBEIJ0KeFAiUQYu3iiWSxzSGQRzKu9jxBKJqNJ03J0F+0jWASCoTkJRZ4pyi36Q2G7MJIOJaJ5pAoYvFzIqVUIdXVl01EGqOt1gn1hvhpVN2vNJsqBu1AG4+HFjEgoMAAIwVOT0DA0NCEAa3JxmKAAH3kPr9AagAFYQ+G5Gg9HoKMD6IGUsGw-IkymIGmBnHMQLBDcUidcNVedQIHRGO9WGxvb7-eNw7nU+moEHNNm49xbCgMNAKkpQQhoDw3chh47nfRXcAAExeyOtjOoLMJtfRuRx+M55OdjdaA+Jo-5rt7swh-k+WD0u6Vx4ql61t4sdjNqNtqAdy8nk42bXmYA7TsOUCjuOk7gRA9qCJEACEiGiMgyCxE6LrLMAADMq4ttGPZnjuyADCBwH-gW3aZpoZ6UVe+7FveZYMvcVavjWdZMJ+TYkdm9GAb2Cb7twCYCdRm60VAg4ziOUBjmQE68LB8HbMhhDocgmELthAAs+E-oJxEEaR8iMWJF5UURFGWQxN5MbID63BWDzEBxrz1jx36tvxtmCcBIk2XmVHkTJEFQYpMFDnBlJQOpaEYfOi4xgZ64SaewEkWR5mHsFXbWRZeXZfZd6OSxT6udWHncR8vEmb5RXpUBwk3qJuXHk1UmFR1oWwZB8nQcp0WOtsRAABYYAgAzAGNfIKqiZxQMgrAYGQyx6EoTnllgz5uc8nEfrV3mqO2fmdQFrVQDQiATrIM3QH4i3Om4RTALI56NdZaZSO1AFmTegxQAA6mNIDWHw2mLgAbKlhE0RddFnT2CMOaWj4uex+3VQ2X58adn3w+G-bSX1EVKVOw2xfFml8EAA

💻 Code

type DistributeOverTupleElementTypes<T> =
    T extends [infer SingleComponent]
       ? (SingleComponent extends unknown // enforce distribution
          ? [SingleComponent]
          : never)
       : (T extends [infer First, ...infer Rest]
          ? (First extends unknown // enforce distribution
             ? (DistributeOverTupleElementTypes<Rest> extends infer EvaluatedRestU
                ? (EvaluatedRestU extends infer FixedEvaluatedRestTuple // enforce distribution
                   ? (FixedEvaluatedRestTuple extends unknown[]
                      ? [First, ...FixedEvaluatedRestTuple]
                      : never)
                   : never)
                : never)
             : never)
          : never);
const t1: [number, string] | [number, 5] | [boolean, string] | [boolean, 5] extends  DistributeOverTupleElementTypes<[number | boolean, string | 5]> ? true : false = true;
const t2: [number, string] | [number, [5]] | [boolean, string] | [boolean, [5]] extends  DistributeOverTupleElementTypes<[number | boolean, string | [5]]> ? true : false = true;  //!! error
const t3: [number, string] | [number, [5]] | [boolean, string] | [boolean, [5]] extends  DistributeOverTupleElementTypes<[number | boolean, string | [5]]> | [boolean, string] ? true : false = true; // !!error
const t4: [number, string] | [number, [5]] | [boolean, string] | [boolean, [5]] extends  DistributeOverTupleElementTypes<[number | boolean, string | [5]]> | [boolean, [5]] ? true : false = true; // !!error
const t5: [number, string] | [number, [5]] | [boolean, string] | [boolean, [5]] extends  DistributeOverTupleElementTypes<[number | boolean, string | [5]]> | [boolean, string] | [boolean, [5]] ? true : false = true;
// Thus, the evaluation result of DistributeOverTupleElementTypes<[number | boolean, string | [5]]> misses the union components [boolean, string] and [boolean, [5]]. Why?

🙁 Actual behavior

Assignments to variables t2, t3 and t4 are flagged as faulty (Type 'true' is not assignable to type 'false'.(2322)).

🙂 Expected behavior

Assignments to variables t2, t3 and t4 are OK because the respectively declared types should evaluate to "true".

Additional information about the issue

No response

@MMF2 MMF2 changed the title Unexpected differences in distribution over union types: type string | 5 is treated differently than type string | [5] Unexpected differences in distribution over union types: type string | 5 behaves differently than type string | [5] May 22, 2025
@jcalz
Copy link
Contributor

jcalz commented May 22, 2025

Is this really the simplest code that demonstrates your issue?

You might not be aware that boolean is not a primitive TS type, but an alias of the union true | false. So you are comparing things like [boolean, X] to [true, X] | [false, X]. Indeed you can use IntelliSense to inspect what DistributeOverTupleElementTypes produces directly (as opposed to doing individual extends tests):

type Z = DistributeOverTupleElementTypes<[boolean, string]>;
// type Z = [false, string] | [true, string]

So what's interesting is that [boolean, X] | [boolean, Y] is apparently seen as assignable to [false, X] | [true, X] | [false, Y] | [true, Y] depending on what X and Y are. I'm guessing this is some edge case around the behavior of #30779. Something like: If you always deal with literal types (like 5 or true or "abc" or null) then TS is happy to try to expand out the unions to match because of #30779... but if you are also involving non-literal types (like number or string or object or any[] or [any]) then it doesn't. This doesn't look like a bug to me, but if it is one, there will be a simpler repro than DistributeOverTupleElementTypes.

@MMF2
Copy link
Author

MMF2 commented May 22, 2025

Yes, you are right. Indeed, I have forgotten that boolean is not a primitive TS type but an abbreviation for true | false. Taking this into consideration I revise the assignment of variable t2 to
const t2: [number, string] | [number, [5]] | [true, string] | [false, string] | [true, [5]] | [false, [5]] extends DistributeOverTupleElementTypes<[number | boolean, string | [5]]> ? true : false = true
and this time it works as expected, i.e., no error.
My fault, sorry!
Still, I don't understand why the type string | 5 behaves differently than string | [5] but that's probably not a bug. Thanks for clarification.

@MMF2 MMF2 closed this as completed May 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants