Skip to content

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

Closed
@MMF2

Description

@MMF2

🔎 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions