MCVE
https://stackblitz.com/edit/typescript-s5uy47?file=index.ts%3AL25
(try using the autocomplete on the parameter of the function f to see what it achieves)
I am trying to create a recursive template literal to check if the strings given to a function satisfy the REST approach of my API.
So far, I managed to make simple examples work (in the code, those would be version & health/check).
What I would like to do is allow routes with parameters in it.
In the provided code, the route users/[PARAM] is accepted with no problem, and offered in the autocomplete of the parameter.
But users/12 is not accepted. For me, the issue is at the end of the Dive type :
`${k & DiveKey}/${typeof param | string}`
If I use ${k & DiveKey}/${typeof param} I get the actual result.
If I use ${k & DiveKey}/${string} then it works, but I don't get users/[PARAM] in the autocomplete of the parameter, which "hides" the path from the developer using the function.
My question is : is there a way to keep users/[PARAM] in the autocomplete, and accept users/12 as a parameter ?
Thank you in advance for the response.
Code
export type RestEndpoint = Dive;
const param = '[PARAM]' as const;
type DiveKey = string | number;
type Dive<T = typeof API_REST_STRUCTURE> = keyof {
[k in keyof T as T[k] extends Record<any, any>
? `${k & DiveKey}/${Dive<T[k]> & DiveKey}`
: T[k] extends typeof param
? `${k & DiveKey}/${typeof param | string}`
: k]: any;
};
const API_REST_STRUCTURE = {
version: false,
health: { check: false },
users: param,
};
function f(p: RestEndpoint) {}
f('version');
f('health/check');
f('users/[PARAM]');
f('users/12');
EDIT 1 : This is what is seen when using ${string} :
And this is what is seen with typeof param (see that users/12 is not accepted)


Currently "pattern" template literal types with placeholders like
`${string}`, as implemented in microsoft/TypeScript#40598 aren't shown in auto-suggest / auto-complete lists. This is a missing feature of TypeScript, as requested in microsoft/TypeScript#41620. For now, if you want to see suggestions corresponding to such types, you'll need to work around it.One approach is to make two types: one with
"[PARAM]"in it, and one withstringin it. Then we make yourf()function generic in such a way that the"[PARAM]"version will be suggested, but anystringwill be accepted in its place.So let's change
Diveso that it only generates the["PARAM"]version:So
RestEndpointSchemahas"[PARAM]"in it. And then we can write a utility type to replace"[PARAM]"withstringwherever it appears:That
Replace<T, S, D>is a tail-recursive conditional type that takes an inputTand produces a version where every appearance ofSis replaced withD. SoRestEndpointisReplace<RestEndpointSchema, typeof param, string>.And now here is the generic function:
The type of
pis a conditional type that depends onT. When you callf()start typing an input, that input will probably not be a validRestEndpoint. And therefore the compiler will inferTas something that doesn't extendRestEndpoint, and so the type ofpwill evaluate toRestEndpointSchema. So you'll see the suggestions including[PARAM]:And then when you start typing, it will accept any valid
RestEndpoint, including the input starting with"users/"without"[PARAM]"in it:but it still rejects invalid inputs:
That's about as close as I can get to your desired behavior. It's a shame that there's no more ergonomic way to do this without generics. The workarounds that work for non-template literal types, as described in microsoft/TypeScript#29729, don't seem to work here. So hopefully microsoft/TypeScript#41620 will eventually be implemented and autosuggestions will include some sort of entries for pattern template literals.
Playground link to code