{
"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"
}