{
  "path": "/3lzrima3bzk2f",
  "site": "at://did:plc:57od6g2ic3e3b3kauctjmo3k/site.standard.publication/3lwagtcm36s2d",
  "$type": "site.standard.document",
  "title": "Distributive Conditional Types in TypeScript",
  "content": {
    "$type": "pub.leaflet.content",
    "pages": [
      {
        "$type": "pub.leaflet.pages.linearDocument",
        "blocks": [
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.blockquote",
              "facets": [
                {
                  "index": {
                    "byteEnd": 57,
                    "byteStart": 50
                  },
                  "features": [
                    {
                      "uri": "https://omg.lol",
                      "$type": "pub.leaflet.richtext.facet#link"
                    }
                  ]
                }
              ],
              "plaintext": "The following post was originally published on my omg.lol Weblog on December 17th, 2023. It's an adaptation of some notes I took on the subject while writing TS."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [],
              "plaintext": ""
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [],
              "plaintext": "In TypeScript, I often define string union types like so:"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.code",
              "language": "typescript",
              "plaintext": "type FavoriteLanguage = 'typescript' | 'golang' | 'elixir';",
              "syntaxHighlightingTheme": "rose-pine"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [],
              "plaintext": "This syntax is useful for when you'd rather use literal string values instead of importing and exporting an enum."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [],
              "plaintext": "We can get more complicated with this sort of union type. Let's say we had some constant data, like so:"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.code",
              "language": "typescript",
              "plaintext": "const langsAndServers = {\n  typescript: \"fresh\",\n  golang: \"gin\",\n  elixir: \"phoenix\"\n} as const;",
              "syntaxHighlightingTheme": "rose-pine"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [],
              "plaintext": "In this situation, we can create two useful string union types: The first, representing the possible top-level keys of the object, and the second--given the top level key as a type argument--describing the value of the key. We might write something like this:"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.code",
              "language": "typescript",
              "plaintext": "type Language = keyof typeof langsAndServers;\n//  type Language = 'typescript' | 'golang' | 'elixir'\n\ntype Server<L extends Language> = (typeof langsAndServers)[L];\ntype Input<L extends Language> = {\n  lang: L\n  server: Server<L>\n}\n\nfunction matchProps<L extends Language>(input: Input<L>){\n  console.log(input)\n}",
              "syntaxHighlightingTheme": "rose-pine"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [],
              "plaintext": "This is great because you can restrict the values of different properties based on the values of others. It's handy when you're building a config-heavy library with lots of options."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.code",
              "language": "typescript",
              "plaintext": "// Valid invocation\nmatchProps({\n  lang: \"typescript\",\n  server: \"fresh\"\n})\n\n// Invalid invocation\nmatchProps({\n  lang: \"typescript\",\n  server: \"gin\"\n       // ^^^^^\n       // Type '\"gin\"' is not assignable to type '\"fresh\"'.\n})",
              "syntaxHighlightingTheme": "rose-pine"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.header",
              "level": 2,
              "facets": [],
              "plaintext": "The Problem"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "index": {
                    "byteEnd": 152,
                    "byteStart": 92
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#bold"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 162,
                    "byteStart": 152
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    },
                    {
                      "$type": "pub.leaflet.richtext.facet#bold"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 231,
                    "byteStart": 162
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#bold"
                    }
                  ]
                }
              ],
              "plaintext": "This setup works great when you're calling the function directly. I ran into an issue where I needed to define an object that satisfied the type of the matchProps argument, as the function would be called with that value elsewhere."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "index": {
                    "byteEnd": 43,
                    "byteStart": 33
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                }
              ],
              "plaintext": "My first instinct was to use the Parameters utility type to create the type I required:"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.code",
              "language": "typescript",
              "plaintext": "type MatchPropsInput = Parameters<typeof matchProps>[0];\n  // ^^^^^^^^^^^^^^^\n  // type MatchPropsInput = {\n  //   lang: \"typescript\" | \"golang\" | \"elixir\";\n  //   server: Server<\"typescript\" | \"golang\" | \"elixir\">;\n  // }",
              "syntaxHighlightingTheme": "rose-pine"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "index": {
                    "byteEnd": 80,
                    "byteStart": 76
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 91,
                    "byteStart": 85
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 148,
                    "byteStart": 138
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 299,
                    "byteStart": 291
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                }
              ],
              "plaintext": "This is not good. In our type definition, we use a generic type to link the lang and server attributes together. However, when we use the Parameters type, we don't have a way to specify a generic, so TypeScript just replaces the generic with the type it inherited from--in our case, the raw Language type union. This results in code that compiles, but is incorrect:"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.code",
              "language": "typescript",
              "plaintext": "const badInput: MatchPropsInput = {\n  lang: \"elixir\",\n  server: \"fresh\"\n       // ^^^^^^^\n       // No error\n}\n\nmatchProps(badInput)\n        // ^^^^^^^^\n        // No error",
              "syntaxHighlightingTheme": "rose-pine"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [],
              "plaintext": "With the little understanding of the TypeScript type system that I had, I was frustrated. After all--I generated this type from the original function definition, so shouldn't I be guaranteed type safety? This is unfortunately not the case."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.header",
              "level": 2,
              "facets": [],
              "plaintext": "Solution: Distributive Conditional Types"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "index": {
                    "byteEnd": 85,
                    "byteStart": 75
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 180,
                    "byteStart": 170
                  },
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#italic"
                    }
                  ]
                }
              ],
              "plaintext": "This is where distributive conditional types come in: Instead of using the Parameters type to generate all of the possible input values, we can use a conditional type to distribute a type with a generic argument across a type union:"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.code",
              "language": "typescript",
              "plaintext": "type DistributeInput<L extends Language> = L extends any ? Input<L> : never;\ntype MatchPropsInput = DistributeInput<Language>;\n  // ^^^^^^^^^^^^^^^\n  // type MatchPropsInput = Input<\"typescript\"> | Input<\"golang\"> | Input<\"elixir\">",
              "syntaxHighlightingTheme": "rose-pine"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [],
              "plaintext": "Now, we can define our input data and trust that TypeScript will prevent us from passing invalid data:"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.code",
              "language": "typescript",
              "plaintext": "const badInput: MatchPropsInput = {\n  lang: \"elixir\",\n  server: \"fresh\"\n       // ^^^^^^^\n       // Type '{ lang: \"elixir\"; server: \"fresh\"; }' is not assignable to type 'MatchPropsInput'.\n       //   Type '{ lang: \"elixir\"; server: \"fresh\"; }' is not assignable to type 'Input<\"elixir\">'.\n       //     Types of property 'server' are incompatible.\n       //       Type '\"fresh\"' is not assignable to type '\"phoenix\"'.\n}",
              "syntaxHighlightingTheme": "rose-pine"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.header",
              "level": 2,
              "facets": [],
              "plaintext": "Documentation"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "index": {
                    "byteEnd": 102,
                    "byteStart": 80
                  },
                  "features": [
                    {
                      "uri": "https://stackoverflow.com/questions/68323706/transform-all-elements-of-union-type",
                      "$type": "pub.leaflet.richtext.facet#link"
                    }
                  ]
                },
                {
                  "index": {
                    "byteEnd": 174,
                    "byteStart": 126
                  },
                  "features": [
                    {
                      "uri": "https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types",
                      "$type": "pub.leaflet.richtext.facet#link"
                    }
                  ]
                }
              ],
              "plaintext": "There were two places where I sourced information on this feature, the first is a StackOverflow thread, and the second is the official docs for TypeScript's Conditional Types."
            }
          }
        ]
      }
    ]
  },
  "bskyPostRef": {
    "cid": "bafyreiceiuk7dfeg2topv3llsfpm7bhlj23ydak3bxn2p6v66xyiphhlty",
    "uri": "at://did:plc:57od6g2ic3e3b3kauctjmo3k/app.bsky.feed.post/3lzrimednck2f",
    "commit": {
      "cid": "bafyreic7oekdg7gszd4sac25g27titjmse5kllrihfljt4plt4iecej6pu",
      "rev": "3lzrimefqpb27"
    },
    "validationStatus": "valid"
  },
  "description": "Alternatively, \"Ouch! My brain!\"",
  "publishedAt": "2025-09-26T22:16:49.494Z"
}