英文:
How to create a type for a comma delimited string of arbitrary item count
问题
I have an object of filter values:
const values = {
search,
users,
categories,
sorting,
}
From this I want to derive a type called FilterSelection that extends strings. Here are some
examples of valid and invalid FilterSelections:
exampleFilterSelections = {
valid: [
"all",
"search,users,sorting",
"-search",
"-search,-users",
],
invalid: [
"any",
"search,-users",
"users,users",
"accounts",
]
}
Its trivial to get individual options:
type IncludedFilter = keyof typeof values
type ExcludedFilter = `-${SelectedFilter}`
Then if I can define FilterInclusions
and FilterExclusions
, I can simply combine them
type FilterSelection = FilterInclusions | FilterExclusions | "all"
However I'm not sure how to define either of these types.
type FilterInclusions =
| IncludedFilter
| `${IncludedFilter},${IncludedFilter}`
| `${IncludedFilter},${IncludedFilter},${IncludedFilter}`
| `${IncludedFilter},${IncludedFilter},${IncludedFilter},${IncludedFilter}`
| `${IncludedFilter},${IncludedFilter},${IncludedFilter},${IncludedFilter},${IncludedFilter}`
| `${IncludedFilter},${IncludedFilter},${IncludedFilter},${IncludedFilter},${IncludedFilter},${IncludedFilter}`
// etc...
But this seems wordy. Is there a better less manual way to do this?
这是您提供的代码片段,不过您提出了一个问题,是否需要我提供解决方案?
英文:
I have an object of filter values:
const values = {
search,
users,
categories,
sorting,
}
From this I want to derive a type called FilterSelection that extends strings. Here are some
examples of valid and invalid FilterSelections:
exampleFilterSelections = {
valid: [
"all",
"search,users,sorting",
"-search",
"-search,-users",
],
invalid: [
"any",
"search,-users",
"users,users",
"accounts",
]
}
Its trivial to get individual options:
type IncludedFilter = keyof typeof values
type ExcludedFilter = `-${SelectedFilter}`
Then if I can define FilterInclussions
and FilterExclusions
, I can simply combine them
type FilterSelection = FilterInclusions | FilterExclusions | "all"
However I'm not sure how to define either of these types.
type FilterInclusions =
| IncludedFilter
| `${IncludedFilter},${IncludedFilter}`
| `${IncludedFilter},${IncludedFilter},${IncludedFilter}`
| `${IncludedFilter},${IncludedFilter},${IncludedFilter},${IncludedFilter}`
| `${IncludedFilter},${IncludedFilter},${IncludedFilter},${IncludedFilter},${IncludedFilter}`
| `${IncludedFilter},${IncludedFilter},${IncludedFilter},${IncludedFilter},${IncludedFilter},${IncludedFilter}`
// etc...
But this seems wordy. Is there a better less manual way to do this?
答案1
得分: 1
以下是您请求的代码部分的中文翻译:
在TypeScript中,很不幸地,没有一种可扩展的方式来编写特定的`FilterSelection`类型。您需要将它写成一个明确的[联合类型](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types),
其中包含每个可接受的字符串[字面类型](https://www.typescriptlang.org/docs/handbook/literal-types.html),因为没有特定的类型可以抽象出您期望的规则。(正如在[microsoft/TypeScript#41160](https://github.com/microsoft/TypeScript/issues/41160)中讨论的那样,TypeScript缺乏*正则表达式验证的字符串类型*,以及在[microsoft/TypeScript#40598](https://github.com/microsoft/TypeScript/pull/40598)中实现的*模式模板文字*只允许像“任意字符串”或“任意数字字符串”甚至“任何大写字符串”等模式)。联合类型最多只能容纳约10万个成员,因此如果您的联合类型大于这个数,您将会遇到问题。由于一组字符串的所有可能排列方式的列表在集合的大小上呈[超指数增长](https://en.wikipedia.org/wiki/Factorial),所以即使只有少数输入字符串,您也很快会遇到限制。
因此,我们放弃了对`FilterSelection`的特定类型。
----
我们能做的最好的事情是编写一个[泛型](https://www.typescriptlang.org/docs/handbook/2/generics.html)类型,如`ValidateFilterSelection<T>`,它的作用类似于对`T`的[约束](https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-constraints),这样只有当`T`是有效的过滤选择时,才会满足`T extends ValidateFilterSelection<T>`。然后,我们可以创建一个泛型辅助函数,如下所示:
```typescript
const filterSelection = <T extends string>(
t: ValidateFilterSelection<T>
) => t as T;
这样,编译器可以推断出T
,而不需要手动编写它。因此,与其编写const f: FilterSelection = "categories,users"
,您可以编写const f = filterSelection("categories,users")
,看起来一样。因此,希望它的行为如下:
const f = filterSelection("categories,users"); // 可以通过
const g = filterSelection("categories,-users"); // 错误
// ---------------------> ~~~~~~~~~~~~~~~~~~~
// 类型“'categories,-users'”的参数不能赋给类型“'categories,users' | 'categories,search' | 'categories,sorting'”的参数
如果您想编写一个它们的数组,您可以创建一个辅助函数,对元组类型进行映射 ValidateFilterSelection<T>
,这样它的行为类似于:
const filterSelections = <T extends string[]>(
t: [...{ [I in keyof T]: ValidateFilterSelection<T[I]> }]
) => t as T;
let exampleFilterSelections = filterSelections([
"all", // 可以通过
"search,users,sorting", // 可以通过
"-search", // 可以通过
"-search,-users", // 可以通过
"search,categories,sorting,users" // 可以通过
"any", // 错误
"search,-users", // 错误
"accounts", // 错误
]);
所以问题是:我们如何编写ValidateFilterSelection<T>
?
以下是一种方法:
type ValidateSelection<V extends string, T extends string, A extends string = ""> =
T extends V ? `${A}${A extends "" ? "" : ","}${T}` :
T extends `${infer F extends V},${infer R}` ? ValidateSelection<
Exclude<V, F>, R, `${A}${A extends "" ? "" : ","}${F}`
> : `${A}${A extends "" ? "" : ","}${V}`
type IncludedFilter = "search" | "users" | "categories" | "sorting"
type ExcludedFilter = `-${IncludedFilter}`
type ValidateFilterSelection<T extends string> =
ValidateSelection<IncludedFilter, T> |
ValidateSelection<ExcludedFilter, T> |
"all";
让我们来看看ValidateSelection<V, T>
是如何工作的:
类型ValidateSelection<V, T>
接受一个有效字符串片段联合类型V
和一个候选字符串类型T
,如果它是这些片段用逗号连接的有效组合,它将返回T
。如果T
无效,那么它将返回在某种程度上与T
相似的有效版本,以便错误消息希望是合理的(例如,“您写了这个,但您可能应该写这个”)。因此,ValidateSelection<"a"|"b"|"c", "a,b">
应该是"a,b"
,但ValidateSelection<"a"|"b"|"c", "b,b">
应该是其他东西,如"b,a" | "b,c"
。
它是一个尾递归条件类型,它将T
拆分为片段,可以在类型参数A
中累积结果;如果T
已经是有效的V
片段之一,那么我们返回A
与T
连接起来。否则,如果T
以F
(有效的V
片段)开始,后跟一个逗号和R
(剩余部分),然后我们递归进入ValidateSelection
,Exclude<V, F>
作为新的V
,R
作为新的T
,A
与
英文:
There's unfortunately no scalable way to write a specific FilterSelection
type in TypeScript. You'd need to write it as an explicit union
of every allowable string literal type, because there's no specific type that abstracts over your desired rules. (TypeScript lacks regular-expression-validated string types as discussed in microsoft/TypeScript#41160, and pattern template literals as implemented in microsoft/TypeScript#40598 only allow patterns like "any string" or "any numeric string" or even "any uppercase string".) Union types can only hold up to about 100,000 members, so if your union is larger than that you'll have a bad time. Since the list of all possible permutations of a set of strings grows super-exponentially in the size of the set, you'll quickly run into the limit for just a handful of input strings.
So we're giving up on a specific type for FilterSelection
.
The best we can do is write a generic type like ValidateFilterSelection<T>
that acts like a constraint on T
, so that T extends ValidateFilterSelection<T>
if and only if T
is a valid filter selection. And then we can make a generic helper function like
const filterSelection = <T extends string>(
t: ValidateFilterSelection<T>
) => t as T;
So that you can have the compiler infer T
instead of requiring that you write it out manually. So instead of const f: FilterSelection = "categories,users"
, you'd write const f = filterSelection("categories,users")
which looks the same if you squint at it right. So the hope is that it would behave like this:
const f = filterSelection("categories,users"); // okay
const g = filterSelection("categories,-users"); // error
// ---------------------> ~~~~~~~~~~~~~~~~~~~
// Argument of type '"categories,-users"' is not assignable to
// parameter of type ....
And if you want to write an array of them you can make a helper function that maps ValidateFilterSeclection<T>
over a tuple type, so it behaves like:
const filterSelections = <T extends string[]>(
t: [...{ [I in keyof T]: ValidateFilterSelection<T[I]> }]
) => t as T;
let exampleFilterSelections = filterSelections([
"all", // okay
"search,users,sorting", // okay
"-search", // okay
"-search,-users", // okay
"search,categories,sorting,users" // okay
"any", // error
"search,-users", // error
"accounts", // error
]);
So the question is: how do we write ValidateFilterSelection<T>
?
Here's one approach:
type ValidateSelection<V extends string, T extends string, A extends string = ""> =
T extends V ? `${A}${A extends "" ? "" : ","}${T}` :
T extends `${infer F extends V},${infer R}` ? ValidateSelection<
Exclude<V, F>, R, `${A}${A extends "" ? "" : ","}${F}`
> : `${A}${A extends "" ? "" : ","}${V}`
type IncludedFilter = "search" | "users" | "categories" | "sorting"
type ExcludedFilter = `-${IncludedFilter}`
type ValidateFilterSelection<T extends string> =
ValidateSelection<IncludedFilter, T> |
ValidateSelection<ExcludedFilter, T> |
"all";
Let's talk about ValidateSelection<V, T>
:
The type ValidateSelection<V, T>
takes a union of valid string pieces V
, and a candidate string type T
and returns T
if it's a valid joining of some of those pieces with commas. If T
is invalid, then it returns a valid version "close" to T
in some sense, so that error messages are hopefully somewhat useful (i.e., "you wrote that but you should probably have written this instead.) So ValidateSelection<"a"|"b"|"c", "a,b">
should be "a,b"
, but ValidateSelection<"a"|"b"|"c", "b,b">
should be something else, like "b,a" | "b,c"
.
It's a tail-recursive conditional type that splits T
into pieces, can accumulates the result in the type parameter A
; if T
is already one of the valid V
pieces, then we return A
concatenated with T
. Otherwise if T
starts F
(a valid V
piece) followed by a comma and R
(the rest of the string), then we recurse into ValidateSelection
with Exclude<V, F>
as the new V
, and with R
as the new T
, and with A
concatenated with F
as the new A
. And finally, if T
does not start with a valid V
, then we return A
concatenated with V
. This last bit is what is responsible for the "close" result. With Validate<"a"|"b"|"c", "b,b">
, it recurses into Validate<"a"|"c", "b", "b">
, and finally becomes "b,a" | "b, c"
.
And then ValidateFilterSelection<T>
is just the union of ValidateSelection<IncludedFilter, T>
, ValidateSelection<ExcludedFilter, T>
, and "all"
, so T extends ValidateFilterSelection<T>
if and only if either T extends ValidateSelection<IncludedFilter, T>
for the include filters, or T extends ValidateSelection<ExcludedFilter, T>
for the exclude filters, or T extends "all"
.
Let's see how it works, although there's just one more change::
const filterSelection = <T extends string>(
t: ValidateFilterSelection<T> & {} // <-- & {} here
) => t as T;
const filterSelections = <T extends string[]>(
t: [...{ [I in keyof T]: ValidateFilterSelection<T[I]> & {} }]
) => t as T;
I added & {}
which doesn't change the types, but it will cause the error message to expand into a union of string literals instead of talking about the ValidateFilterSelection
name.
And testing:
const f = filterSelection("categories,users"); // okay
const g = filterSelection("categories,-users"); // error
// ---------------------> ~~~~~~~~~~~~~~~~~~~
// Argument of type '"categories,-users"' is not assignable
// to parameter of type '"-search" | "-users" | "-categories" | "-sorting" |
// "all" | "categories,users" | "categories,search" | "categories,sorting"'
let exampleFilterSelections = filterSelections([
"all", // okay
"search,users,sorting", // okay
"-search", // okay
"-search,-users", // okay
"search,categories,sorting,users" // okay
"any", // error
// Type '"any"' is not assignable to type '"search" | "users"
// | "categories" | "sorting" | "-search" | "-users" |
// "-categories" | "-sorting" | "all"'.
"search,-users", // error
// Type '"search,-users"' is not assignable to type '"-search" | "-users" | "-categories"
// | "-sorting" | "all" | "search,users" | "search,categories" |
// "search,sorting"'. Did you mean '"search,users"'?
"accounts", // error
// Type '"accounts"' is not assignable to type '"search" | "users" |
// "categories" | "sorting" | "-search" | "-users" |
// "-categories" | "-sorting" | "all"'.
])
Looks good. Valid strings are accepted and invalid ones are rejected, and the error messages make reasonable-ish suggestions (maybe "search,-users"
shouldn't mention "-sorting"
, but the Did you mean '"search,users"'?
is nice).
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论