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 with string
in it. Then we make your f()
function generic in such a way that the "[PARAM]"
version will be suggested, but any string
will be accepted in its place.
So let's change Dive
so that it only generates the ["PARAM"]
version:
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}`
: K]: any;
};
type RestEndpointSchema = Dive;
// type RestEndpointSchema = "version" | "health/check" | "users/[PARAM]"
So RestEndpointSchema
has "[PARAM]"
in it. And then we can write a utility type to replace "[PARAM]"
with string
wherever it appears:
type Replace<
T extends string, S extends string, D extends string,
A extends string = ""
> = T extends `${infer F}${S}${infer R}` ?
Replace<R, S, D, `${A}${F}${D}`> : `${A}${T}`
type RestEndpoint = Replace<RestEndpointSchema, typeof param, string>
// type RestEndpoint = "version" | "health/check" | `users/${string}`
That Replace<T, S, D>
is a tail-recursive conditional type that takes an input T
and produces a version where every appearance of S
is replaced with D
. So RestEndpoint
is Replace<RestEndpointSchema, typeof param, string>
.
And now here is the generic function:
function f<T extends string>(
p: T extends RestEndpoint ? T : RestEndpointSchema
) { }
The type of p
is a conditional type that depends on T
. When you call f()
start typing an input, that input will probably not be a valid RestEndpoint
. And therefore the compiler will infer T
as something that doesn't extend RestEndpoint
, and so the type of p
will evaluate to RestEndpointSchema
. So you'll see the suggestions including [PARAM]
:
f(); // f(p: "version" | "health/check" | "users/[PARAM]"): void
f('')
// ^ health/check
// users/[PARAM]
// version
And then when you start typing, it will accept any valid RestEndpoint
, including the input starting with "users/"
without "[PARAM]"
in it:
f('version'); // okay
f('health/check'); // okay
f('users/12'); // okay
but it still rejects invalid inputs:
f('dog'); // error
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.