{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreibx5ciwxssz3wacfsjadcnjk7ecauan4rqyb7aclg5hgkg56u7bim",
"uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mpmcj6ixz6e2"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreihef7umy4tkpanhxlqrtaq3zdmi2jxj5neydz37bkslazz37ertim"
},
"mimeType": "image/webp",
"size": 22788
},
"path": "/svs-nickm/typescript-infers-the-last-overload-so-i-changed-the-order-3018",
"publishedAt": "2026-07-01T19:46:10.000Z",
"site": "https://dev.to",
"tags": [
"typescript",
"webdev",
"programming",
"javascript"
],
"textContent": "Once I was working with `i18next` and wanted to get better inference from translation keys.\n\nUsually, with translation libraries, we pass keys as strings:\n\n\n\n translate(\"user.profile.title\");\n\n\nAnd strings are fine... until they are not.\n\nI wanted something closer in spirit to C# expression trees. Not real expression trees, of course — JavaScript does not have those. But something like this:\n\n\n\n translate($ => $.user.profile.title);\n\n\nUnder the hood, this could be handled with a `Proxy`, reading the full member-access path at runtime.\n\nWhy even bother?\n\nBecause a lambda selector can give you much better DX than a plain string:\n\n * you can navigate to the source JSON/schema field;\n * you can navigate to the translation schema definition;\n * you can rename fields with editor refactoring;\n * you can get autocomplete through the whole translation tree.\n\n\n\nCool idea. But then I had to answer a very annoying TypeScript question.\n\nI needed to take an existing function type from `i18next`, replace one of its argument types, and preserve the rest of the API.\n\nSounds simple enough, right?\n\nExcept that function type had several call signatures.\n\nIn other words: overloads.\n\nAnd that led me to the real question:\n\n> Can we extract all call signatures from an overloaded function type, transform them, and build the overloaded type back?\n\nI searched around and mostly found hardcoded overload tables, like “support up to 5 overloads”, “support up to 10 overloads”, etc.\n\nThat was not exactly what I wanted.\n\nSo I started experimenting.\n\n## The first wall: TypeScript infers only one overload\n\nLet’s start with a tiny helper:\n\n\n\n type InferCallSignature<T_Callable> =\n (\n T_Callable extends (...args: infer T_Parameters) => infer T_Result\n ? ((...parameters: T_Parameters) => T_Result)\n : never\n );\n\n\nNothing special here. If `T_Callable` is callable, infer its parameters and return type, then rebuild a normal function type.\n\nNow let’s try it with an overloaded callable type:\n\n\n\n type A =\n {\n (): void;\n (a: number): string;\n (a: string, b: number): boolean;\n };\n\n\n type B = InferCallSignature<A>;\n // ^? type B = (a: string, b: number) => boolean\n\n\nTypeScript inferred only one call signature.\n\nMore specifically, it inferred the **last** call signature.\n\nThat is the documented behavior for overloaded functions: when inferring from an overloaded type, TypeScript uses the last signature.\n\nSo at first glance, we are stuck.\n\nWe can get this:\n\n\n\n type B = (a: string, b: number) => boolean;\n\n\nBut not this:\n\n\n\n type All =\n (\n (() => void)\n |\n ((a: number) => string)\n |\n ((a: string, b: number) => boolean)\n );\n\n\n\nAnd without such a union, we cannot easily transform every overload.\n\n## Then I tried an intersection\n\nAt this point, I started thinking about order.\n\nWe already know call-signature order matters: TypeScript infers from the last overload. So I wanted to poke the only thing I could still poke: intersection order.\n\nThat is what made me try something that looked completely redundant: intersecting the overloaded type with one of its existing call signatures.\n\nBut I did not only try this order:\n\n\n\n declare const aLeft: A & ((a: string, b: number) => boolean);\n\n\nI also tried the opposite order:\n\n\n\n declare const aRight: ((a: string, b: number) => boolean) & A;\n\n\nAnd that order is the important part.\n\nLogically, neither type should change much.\n\n`A` already has this signature:\n\n\n\n (a: string, b: number): boolean;\n\n\nSo all of these should behave the same when called:\n\n\n\n declare const a: A;\n\n a();\n a(123);\n a(\"hello\", 1);\n\n aLeft();\n aLeft(123);\n aLeft(\"hello\", 1);\n\n aRight();\n aRight(123);\n aRight(\"hello\", 1);\n\n\nAnd they do.\n\nBut then I checked IntelliSense.\n\nFor plain `A`, VS Code showed the first overload as:\n\n\n\n a(): void\n\n\nFor this version:\n\n\n\n type Left = A & ((a: string, b: number) => boolean);\n\n\nit still looked functionally the same as `A`: the same call signatures, in the same order.\n\nBut for the opposite order:\n\n\n\n type Right = ((a: string, b: number) => boolean) & A;\n\n\nVS Code showed the first overload as:\n\n\n\n aRight(a: string, b: number): boolean\n\n\nWait.\n\nThe left operand of `&` was placed in front of the right operand in the call-signature order.\n\nThat was the interesting part.\n\nThe call surface looked the same, but the overload order changed. And even more importantly, `A & Signature` was not behaving the same as `Signature & A` for overload ordering.\n\n## Why order matters\n\nRemember the previous limitation:\n\n> TypeScript infers from the last call signature.\n\nSo if intersections can change overload order, maybe we can control what `infer` sees.\n\nLet’s test it:\n\n\n\n type LastCallSignature = InferCallSignature\n <\n ((a: string, b: number) => boolean) & A\n >;\n\n // ^? type LastCallSignature = (a: number) => string\n\n\nAnd there it is.\n\nBefore the intersection, TypeScript inferred:\n\n\n\n (a: string, b: number) => boolean\n\n\nAfter intersecting that same signature back into `A`, TypeScript inferred:\n\n\n\n (a: number) => string\n\n\nSo the trick is:\n\n> TypeScript infers the last overload. Intersections can change the overload alignment. Therefore, intersections can change what TypeScript infers.\n\nThat is the whole door.\n\nOnce I saw that, extracting all call signatures became possible.\n\n## The plan\n\nIf TypeScript gives us only one call signature at a time, we can do this recursively:\n\n 1. Infer the currently visible last call signature.\n 2. Add that signature into an intersection alignment.\n 3. Let TypeScript expose another signature on the next pass.\n 4. Repeat until the alignment already satisfies the original callable type.\n\n\n\n## The utility type\n\nHere is the full version:\n\n\n\n type InternalCallSignatures<T_Callable, T_Alignment> =\n (\n ({ [key in keyof T_Callable]: T_Callable[key] } & T_Alignment) extends T_Callable\n ? never\n : T_Callable extends (...args: infer T_Parameters) => infer T_Result\n ?\n (\n ((...parameters: T_Parameters) => T_Result)\n |\n InternalCallSignatures\n <\n T_Alignment & T_Callable,\n ((...parameters: T_Parameters) => T_Result) & T_Alignment\n >\n )\n : never\n );\n\n export type CallSignatures<T_Callable> =\n InternalCallSignatures<T_Callable, {}>;\n\n\nLet’s unpack it.\n\n`T_Callable` is the callable type we are currently inspecting.\n\n`T_Alignment` is the accumulated intersection of signatures we have already discovered. Think of it as a type-level alignment state. It influences which overload TypeScript will infer next.\n\nThis part is the stop condition:\n\n\n\n ({ [key in keyof T_Callable]: T_Callable[key] } & T_Alignment) extends T_Callable\n ? never\n : ...\n\n\nIn human words:\n\n> Does the current alignment already satisfy the callable shape we are inspecting?\n\nIf yes, we are done, so we return `never`.\n\nIf not, we infer one more call signature:\n\n\n\n T_Callable extends (...args: infer T_Parameters) => infer T_Result\n ? ((...parameters: T_Parameters) => T_Result)\n : never\n\n\nAnd then recurse:\n\n\n\n InternalCallSignatures\n <\n T_Alignment & T_Callable,\n ((...parameters: T_Parameters) => T_Result) & T_Alignment\n >\n\n\nOn every step, we return the inferred signature as part of a union, and we extend the alignment with that signature.\n\nThe mapped type here:\n\n\n\n { [key in keyof T_Callable]: T_Callable[key] }\n\n\nmay look useless, but it helps normalize the non-callable/object part of the type before checking it together with the current alignment.\n\n## The result\n\nNow let’s try it with our overloaded type:\n\n\n\n type A =\n {\n (): void;\n (a: number): string;\n (a: string, b: number): boolean;\n };\n\n\n type AllCallSignatures = CallSignatures<A>;\n\n\nAnd the result is:\n\n\n\n type AllCallSignatures =\n (\n (() => void)\n |\n ((a: number) => string)\n |\n ((a: string, b: number) => boolean)\n );\n\n\nBeautiful.\n\nNow the overloads are not trapped inside an overloaded callable type anymore.\n\nThey are a union.\n\nAnd unions are much easier to transform.\n\n## Transforming the signatures\n\nFor example, let’s wrap every return type into `Promise`:\n\n\n\n type PromisifySignature<T_Signature> =\n (\n T_Signature extends (...args: infer T_Parameters) => infer T_Result\n ? (...parameters: T_Parameters) => Promise<T_Result>\n : never\n );\n\n\nBecause conditional types distribute over unions, this works naturally:\n\n\n\n type AsyncCallSignatures = PromisifySignature<CallSignatures<A>>;\n\n\nResult:\n\n\n\n type AsyncCallSignatures =\n (\n (() => Promise<void>)\n |\n ((a: number) => Promise<string>)\n |\n ((a: string, b: number) => Promise<boolean>)\n );\n\n\nNow we can convert the union back into an intersection:\n\n\n\n type UnionToIntersection<T_Union> =\n (\n (\n T_Union extends unknown\n ? (value: T_Union) => void\n : never\n ) extends (value: infer T_Intersection) => void\n ? T_Intersection\n : never\n );\n\n\nAnd build an overloaded callable type again:\n\n\n\n type AsyncA = UnionToIntersection<AsyncCallSignatures>;\n\n\nUsage:\n\n\n\n declare const asyncA: AsyncA;\n\n const result1 = asyncA();\n // ^? Promise<void>\n\n const result2 = asyncA(123);\n // ^? Promise<string>\n\n const result3 = asyncA(\"hello\", 1);\n // ^? Promise<boolean>\n\n\nSo now the full pipeline is:\n\n\n\n // overloads -> union -> transform -> intersection -> overloads again\n\n\n## Back to the original problem\n\nIn my `i18next` experiment, this meant I could theoretically take overloaded function signatures, replace one argument type, and preserve the rest of the overload behavior.\n\nThe important part was not `i18next` itself.\n\nThe important part was realizing this:\n\n> If you can turn overloads into a union, you can operate on them like data.\n\nAnd the key that made it possible was surprisingly small:\n\n> TypeScript infers the last overload, but intersections can change overload order.\n\n## Final note\n\nThis is definitely a type-system trick, not an official “overload reflection API”.\n\nIt relies on how TypeScript currently evaluates overloaded and intersected callable types. So if you use something like this in a library, test it carefully with the TypeScript versions you support.\n\nStill, I think it is a beautiful little trick.\n\nI started with translation keys and ended up learning that `&` can do much more than just merge object types.\n\nSometimes TypeScript is weird in exactly the right way.",
"title": "TypeScript Infers the Last Overload... So I Changed the Order"
}