英文:
Typescript : recursive template literal type, allow both string and specific chaining
问题
以下是您要翻译的内容:
MCVE
https://stackblitz.com/edit/typescript-s5uy47?file=index.ts%3AL25
(尝试在函数 f
参数上使用自动补全,看看它实现了什么)
我正在尝试创建一个递归模板文字,以检查传递给函数的字符串是否满足我的API的REST方法。
到目前为止,我成功使简单的示例工作(在代码中,这些示例是 version
和 health/check
)。
我想要做的是允许带有参数的路由。
在提供的代码中,路由 users/[PARAM]
可以被接受并在参数的自动补全中提供,没有问题。
但是 users/12
不被接受。对我来说,问题在于 Dive
类型的末尾:
`${k & DiveKey}/${typeof param | string}`
如果我使用 ${k & DiveKey}/${typeof param}
,我会得到实际的结果。
如果我使用 ${k & DiveKey}/${string}
,那么它可以工作,但是我无法在参数的自动补全中得到 users/[PARAM]
,这会让使用该函数的开发人员无法看到路径。
我的问题是:有没有办法保持参数的自动补全中出现 users/[PARAM]
,并接受 users/12
作为参数?
非常感谢您提前的回复。
Code
<!-- begin snippet: js hide: false console: true babel: false -->
<!-- language: lang-js -->
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');
<!-- end snippet -->
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)
英文:
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
<!-- begin snippet: js hide: false console: true babel: false -->
<!-- language: lang-js -->
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');
<!-- end snippet -->
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)
答案1
得分: 1
目前,“pattern” template literal types 中的占位符,如 `${string}`
,正如在 microsoft/TypeScript#40598 中实现的那样,没有显示在自动建议/自动完成列表中。这是 TypeScript 的一个缺失功能,正如在 microsoft/TypeScript#41620 中所请求的那样。暂时,如果您想要看到与这些类型对应的建议,您需要绕过它。
一种方法是创建两种类型:一种包含 "[PARAM]"
,另一种包含 string
。然后,我们以这样的方式使您的 f()
函数成为 generic,以便 "[PARAM]"
版本将被建议,但任何 string
都将被接受。
因此,让我们更改 Dive
,使其仅生成 ["PARAM"]
版本:
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]"
所以 RestEndpointSchema
包含 "[PARAM]"
。然后,我们可以编写一个实用类型,以在出现 "[PARAM]"
的地方将其替换为 string
:
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}`
那个 Replace<T, S, D>
是一个 tail-recursive conditional type,它接受一个输入 T
并生成一个版本,其中每次出现 S
都被替换为 D
。因此,RestEndpoint
是 Replace<RestEndpointSchema, typeof param, string>
。
现在这是泛型函数:
function f<T extends string>(
p: T extends RestEndpoint ? T : RestEndpointSchema
) { }
p
的类型是一个 conditional type,它取决于 T
。当您调用 f()
并开始键入输入时,该输入可能不会是有效的 RestEndpoint
。因此编译器将推断 T
为不扩展 RestEndpoint
的内容,因此 p
的类型将评估为 RestEndpointSchema
。因此,您将看到包括 [PARAM]
的建议:
f(); // f(p: "version" | "health/check" | "users/[PARAM]"): void
f('')
// ^ health/check
// users/[PARAM]
// version
然后,当您开始键入时,它将接受任何有效的 RestEndpoint
,包括以 "users/"
开头而没有 "[PARAM]"
的输入:
f('version'); // okay
f('health/check'); // okay
f('users/12'); // okay
但它仍然会拒绝无效的输入:
```typescript
f('dog'); // error
这是我能够接近所需行为的方法。很遗憾,在没有泛型的情况下,似乎没有更符合人体工程学的方法来实现这一点。在非模板文字类型的情况下有效的解决方法,如 microsoft/TypeScript#29729 中所描述的,似乎在这里不起作用。所以希望 microsoft/TypeScript#41620 最终会得以实施,自动建议将包括某种形式的模板文字类型的条目。
英文:
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论