Improve type inference and navigation for custom string literal / mapped path types in TypeScript
Hello.
I'm working on a bit of typescript to re-map a hash of functions into a single function which can call those functions by path.
It is fairly advanced TypeScript (thanks to Claude AI for helping me work through my ideas to working code!).
The function returned by makeHandler<typeof handlers>()
is typed so that the first parameter is a union type of all the possible paths of all the nested objects, so that a user may call reference a target via a string, i.e. ‘a.b.c’
:
const handlers = {
a: {
b: {
c () {
return 'hello'
}
}
}
}
const result = call('a.b.c').charAt(1) // "e"
The TypeScript to generate this type is fairly advanced, and is shown below (you can run it in TS playground or Quokka or the like):
type Handler = (...args: any[]) => any
type Handlers = {
[key: string]: Handlers | Handler;
};
type HandlerPaths<T extends Handlers, Prefix extends string = ''> = {
[K in keyof T]: T[K] extends Handlers
? HandlerPaths<T[K], `${Prefix}${K & string}.`>
: `${Prefix}${K & string}`;
}[keyof T];
type HandlerParameters<T extends Handlers, P extends string> =
P extends keyof T
? T[P] extends Handler
? T[P]
: never
: P extends `${infer First}.${infer Rest}`
? First extends keyof T
? T[First] extends Handlers
? HandlerParameters<T[First], Rest>
: never
: never
: never;
// factory function to create a handler object
function makeHandler<T extends Handlers>() {
// @ts-ignore
return function call<P extends HandlerPaths<T>>(path: P, ...args: Parameters<HandlerParameters<T, P>>): ReturnType<HandlerParameters<T, P>> {
console.log(`Called with path: ${path}`)
// Implement sending here
return undefined as ReturnType<HandlerParameters<T, P>> // placeholder
}
}
// handlers hash
const handlers = {
foo(str: string): number { return str.length },
bar(num: number): string { return num.toString() },
nested: {
baz(data: { bool: boolean, num?: number }): Date {
return new Date()
},
qux: {
quux() {
return 'HELLO!'
},
},
},
}
// build call handler
const call = makeHandler<typeof handlers>()
// call functions
const result1 = call('foo', 'hello')
const result2 = call('bar', 123)
const result3 = call('nested.baz', { bool: true, num: 123 })
const result4 = call('nested.qux.quux').charAt(0)
Even better, in WebStorm, you get auto-complete and you can even Cmd
+Click
the first parameter and it will take you directly to the handler!
This is clearly amazing and thank you to WebStorm / MicroSoft for making this possible.
Code auto-complete works if HandlerPaths
uses a dot (.
) or a slash (/
) as the delimiter:
However, code navigation only works if the delimited string uses a dot (.
) as the delimiter.
If the delimiter is changed to a slash (/
), then WebStorm ceases to connect the string path to the handler block, and the linking UX disappears for all but the top-level keys:
type HandlerPaths<T extends Handlers, Prefix extends string = ''> = {
[K in keyof T]: T[K] extends Handlers
? HandlerPaths<T[K], `${Prefix}${K & string}/`> // <-- see / at end of string
: `${Prefix}${K & string}`;
}[keyof T];
The fact that TypeScript connects the dots (.
) (no pun intended) in the first place is amazing, but it's strange that it doesn't work with a slash (/
). And for my use case, a slash would make much more sense.
Is this the TypeScript Language Service or WebStorm with the problem here?
Is there a way round it, or am I just stuck with dots if I want code navigation?
Many thanks,
Dave
Please sign in to leave a comment.
Please could you share a complete code snippet with slashes? With the updated
HandlerPaths
type definition, the code likedoesn't compile.
Hey Elena,
Have been working a lot more on the code and I think what is happening is that WS is just guessing where the code is; there is no real type trail back through that string.
I've also discovered there are some big issues with the TS engine and infinite recursion which means I'm moving away from that approach, and onto a more limited, manual approach.
But; let me get a working version up and running, then we can go from there.