Typescript:递归模板文字类型,允许字符串和特定链式连接。

huangapple go评论65阅读模式
英文:

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方法。

到目前为止,我成功使简单的示例工作(在代码中,这些示例是 versionhealth/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 = &#39;[PARAM]&#39; as const;
type DiveKey = string | number;

type Dive&lt;T = typeof API_REST_STRUCTURE&gt; = keyof {
	[k in keyof T as T[k] extends Record&lt;any, any&gt;
		? `${k &amp; DiveKey}/${Dive&lt;T[k]&gt; &amp; DiveKey}`
		: T[k] extends typeof param
		? `${k &amp; DiveKey}/${typeof param | string}`
		: k]: any;
};

const API_REST_STRUCTURE = {
  version: false,
  health: { check: false },
	users: param,
};

function f(p: RestEndpoint) {}

f(&#39;version&#39;);
f(&#39;health/check&#39;);
f(&#39;users/[PARAM]&#39;);
f(&#39;users/12&#39;);

<!-- end snippet -->

EDIT 1 : This is what is seen when using ${string} :

Typescript:递归模板文字类型,允许字符串和特定链式连接。

And this is what is seen with typeof param (see that users/12 is not accepted)

Typescript:递归模板文字类型,允许字符串和特定链式连接。

英文:

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 &amp; DiveKey}/${typeof param | string}`

If I use ${k &amp; DiveKey}/${typeof param} I get the actual result.

If I use ${k &amp; 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 = &#39;[PARAM]&#39; as const;
type DiveKey = string | number;

type Dive&lt;T = typeof API_REST_STRUCTURE&gt; = keyof {
	[k in keyof T as T[k] extends Record&lt;any, any&gt;
		? `${k &amp; DiveKey}/${Dive&lt;T[k]&gt; &amp; DiveKey}`
		: T[k] extends typeof param
		? `${k &amp; DiveKey}/${typeof param | string}`
		: k]: any;
};

const API_REST_STRUCTURE = {
  version: false,
  health: { check: false },
	users: param,
};

function f(p: RestEndpoint) {}

f(&#39;version&#39;);
f(&#39;health/check&#39;);
f(&#39;users/[PARAM]&#39;);
f(&#39;users/12&#39;);

<!-- end snippet -->

EDIT 1 : This is what is seen when using ${string} :

Typescript:递归模板文字类型,允许字符串和特定链式连接。

And this is what is seen with typeof param (see that users/12 is not accepted)

Typescript:递归模板文字类型,允许字符串和特定链式连接。

答案1

得分: 1

目前,“pattern” template literal types 中的占位符,如 `${string}`,正如在 microsoft/TypeScript#40598 中实现的那样,没有显示在自动建议/自动完成列表中。这是 TypeScript 的一个缺失功能,正如在 microsoft/TypeScript#41620 中所请求的那样。暂时,如果您想要看到与这些类型对应的建议,您需要绕过它。

一种方法是创建两种类型:一种包含 &quot;[PARAM]&quot;,另一种包含 string。然后,我们以这样的方式使您的 f() 函数成为 generic,以便 &quot;[PARAM]&quot; 版本将被建议,但任何 string 都将被接受。

因此,让我们更改 Dive,使其仅生成 [&quot;PARAM&quot;] 版本:

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 = &quot;version&quot; | &quot;health/check&quot; | &quot;users/[PARAM]&quot;

所以 RestEndpointSchema 包含 &quot;[PARAM]&quot;。然后,我们可以编写一个实用类型,以在出现 &quot;[PARAM]&quot; 的地方将其替换为 string

type Replace<
    T extends string, S extends string, D extends string,
    A extends string = &quot;&quot;
> = 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 = &quot;version&quot; | &quot;health/check&quot; | `users/${string}`

那个 Replace<T, S, D> 是一个 tail-recursive conditional type,它接受一个输入 T 并生成一个版本,其中每次出现 S 都被替换为 D。因此,RestEndpointReplace<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: &quot;version&quot; | &quot;health/check&quot; | &quot;users/[PARAM]&quot;): void
f('')
// ^ health/check
//   users/[PARAM]
//   version

然后,当您开始键入时,它将接受任何有效的 RestEndpoint,包括以 &quot;users/&quot; 开头而没有 &quot;[PARAM]&quot; 的输入:

f('version'); // okay
f('health/check'); // okay
f('users/12'); // okay

但它仍然会拒绝无效的输入:

```typescript
f('dog'); // error

这是我能够接近所需行为的方法。很遗憾,在没有泛型的情况下,似乎没有更符合人体工程学的方法来实现这一点。在非模板文字类型的情况下有效的解决方法,如 microsoft/TypeScript#29729 中所描述的,似乎在这里不起作用。所以希望 microsoft/TypeScript#41620 最终会得以实施,自动建议将包括某种形式的模板文字类型的条目。

[代码播放链接](https://www.typescriptlang.org/play?#code/MYewdgzgLgBADgQwE4ILYwLwwOQG0AKAggEqECyAutjAhDKJFANwBQLUAnnAKYwAiASwBu3ANLcOmGNCQCwAcxgAfGGACuqAEbckrdl16CRAHgAqUzjxAAzGIXwBJAPrEAogGVTTz8QCqAYVNfNwA+KQBrCRsYAG8WGASYXFEYORhIjmjzWhhTZIoYbgAPKG4wABM6Ym5QJHLjBDAOABoaJpD4xIB+GAADABIYlIAyfmExCQBfAHpBo24zfLDR+fEOSd7OhIAuXPzCkrLKmEtuaMQUVC2YHoGhmBXxtZnB0-PkNA3r3dEKXcaOKxJnoGNA7I4XB4vD4AkE3FI4okYCIkBABOBdtYEAAbCDcZrXAAW3BxUEJuxi9GJwHCmJxeJgkwJSLUeNRuwuaAJwLYpxg1Tg2IQwAW13MxVKFToMjk8la7gOkuOMoUrT4iqO0qgslV10IG

英文:

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 &quot;[PARAM]&quot; in it, and one with string in it. Then we make your f() function generic in such a way that the &quot;[PARAM]&quot; version will be suggested, but any string will be accepted in its place.

So let's change Dive so that it only generates the [&quot;PARAM&quot;] version:

type Dive&lt;T = typeof API_REST_STRUCTURE&gt; = keyof {
    [K in keyof T as T[K] extends Record&lt;any, any&gt;
    ? `${K &amp; DiveKey}/${Dive&lt;T[K]&gt; &amp; DiveKey}`
    : T[K] extends typeof param
    ? `${K &amp; DiveKey}/${typeof param}`
    : K]: any;
};

type RestEndpointSchema = Dive;
// type RestEndpointSchema = &quot;version&quot; | &quot;health/check&quot; | &quot;users/[PARAM]&quot;

So RestEndpointSchema has &quot;[PARAM]&quot; in it. And then we can write a utility type to replace &quot;[PARAM]&quot; with string wherever it appears:

type Replace&lt;
    T extends string, S extends string, D extends string,
    A extends string = &quot;&quot;
&gt; = T extends `${infer F}${S}${infer R}` ?
    Replace&lt;R, S, D, `${A}${F}${D}`&gt; : `${A}${T}`

type RestEndpoint = Replace&lt;RestEndpointSchema, typeof param, string&gt;
// type RestEndpoint = &quot;version&quot; | &quot;health/check&quot; | `users/${string}`

That Replace&lt;T, S, D&gt; 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&lt;RestEndpointSchema, typeof param, string&gt;.

And now here is the generic function:

function f&lt;T extends string&gt;(
    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: &quot;version&quot; | &quot;health/check&quot; | &quot;users/[PARAM]&quot;): void
f(&#39;&#39;)
// ^ health/check
//   users/[PARAM]
//   version

And then when you start typing, it will accept any valid RestEndpoint, including the input starting with &quot;users/&quot; without &quot;[PARAM]&quot; in it:

f(&#39;version&#39;); // okay
f(&#39;health/check&#39;); // okay
f(&#39;users/12&#39;); // okay

but it still rejects invalid inputs:

f(&#39;dog&#39;); // 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.

Playground link to code

huangapple
  • 本文由 发表于 2023年7月4日 22:49:47
  • 转载请务必保留本文链接:https://go.coder-hub.com/76613803.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定