Skip to content

Experiment: Distribute Control Flow#2629

Open
devanshj wants to merge 1 commit intomicrosoft:mainfrom
devanshj:distribute-control-flow
Open

Experiment: Distribute Control Flow#2629
devanshj wants to merge 1 commit intomicrosoft:mainfrom
devanshj:distribute-control-flow

Conversation

@devanshj
Copy link

@devanshj devanshj commented Feb 1, 2026

Sometimes it's super easy to encounter incompleteness in the compiler...

type Layer = 
  | { labels: { selectedFieldName: "A" | "B" } }
  | { labels: { selectedFieldName: "C" } }

declare let layers: Layer[]

// should compile but doesn't
let newLayers: Layer[] = layers.map(layer => {
//  ~~~~~~~~~
// Type '{ labels: { selectedFieldName: "A" | "B"; } | { selectedFieldName: "C"; }; }[]' is not assignable to type 'Layer[]'.
//   Type '{ labels: { selectedFieldName: "A" | "B"; } | { selectedFieldName: "C"; }; }' is not assignable to type 'Layer'.
//     Type '{ labels: { selectedFieldName: "A" | "B"; } | { selectedFieldName: "C"; }; }' is not assignable to type '{ labels: { selectedFieldName: "C"; }; }'.
//       Types of property 'labels' are incompatible.
//         Type '{ selectedFieldName: "A" | "B"; } | { selectedFieldName: "C"; }' is not assignable to type '{ selectedFieldName: "C"; }'.
//           Type '{ selectedFieldName: "A" | "B"; }' is not assignable to type '{ selectedFieldName: "C"; }'.
//             Types of property 'selectedFieldName' are incompatible.
//               Type '"A" | "B"' is not assignable to type '"C"'.
//                 Type '"A"' is not assignable to type '"C"'.ts(2322)
  return { labels: layer.labels }
})

Of course here you can just refactor this into layers.map(layer => layer) as it doesn't do anything but that's not always the case.

So how would tell the compiler that it's safe to compile this? I would just go with an assertion somewhere but there's also a stupid way to solve it...

let newLayers: Layer[] = layers.map(layer => {
  switch (layer.labels.selectedFieldName) {
    case "A": return { labels: layer.labels }
    case "B": return { labels: layer.labels }
    case "C": return { labels: layer.labels }
  }
})

Ha! Stupid as it may be but it does unlock an insight... what if there was a way to tell the compiler to do this same thing without having to write all those cases... maybe something like the following?

let newLayers: Layer[] = layers.map(layer => {
  distribute (layer) {
    return { labels: layer.labels }
  }
})

And that's the exact thing this experiment implements.

It also solves incompleteness like the following...

type A = { type: "a", value: boolean }
type B = { type: "b", value: string }

const handlers = {
  a: (action: A) => {},
  b: (action: B) => {},
}

const handle = (action: A | B) => {
  handlers[action.type](action)
//                      ~~~~~~
// Argument of type 'A | B' is not assignable to parameter of type 'never'.
//   The intersection 'A & B' was reduced to 'never' because property 'type' has conflicting types in some constituents.
//     Type 'A' is not assignable to type 'never'.ts(2345)

  distribute (action) {
    handlers[action.type](action)
  }
}

distribute (x) { y } is an erasable syntax that compiles to { y }. And the compiler checks y for each member of type of x. I know that sounds like a nightmare if x is a big union or if y has many nodes. But we can always put limit to those things. And at the end this is just an experiment to have fun!

Fixes microsoft/TypeScript#25051 (that's the main inspiration for this PR cc @jcalz)
Fixes microsoft/TypeScript#30581 (that's already "fixed" but not all cases are fixed and distribute has nicer DX as there's no ceremony needed)
Probably fixes a ton of issues that I'm not aware of. Because it enables you to overcome the most basic incompleteness that @RyanCavanaugh talks about here

Let me know what y'all think and as always feel free to close this PR if y'all think this won't take off. Thanks for reading!

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

Successfully merging this pull request may close these issues.

Wishlist: support for correlated union types Suggestion: Opt-in distributive control flow analysis

1 participant