Typescript 中逗号分隔字符串的真正递归模板文字
Truly recursive Template Literal for comma-separated strings in Typescript
我正在尝试为包含逗号分隔值的字符串定义 Typescript 模板文字。我可以使这个定义真正递归和通用吗?
请参阅 this typescript playground 以试验案例。
每个逗号分隔值代表一个排序顺序,如 height asc
。该字符串应定义一个顺序(包括主要、次要、第三等),根据有效字段名称的并集和两个可能的顺序 "asc"
和 "desc"
,可以包含无限多的排序级别,由逗号按照示例代码中的示例。
下面的实现最多处理 4 个排序顺序,但案例 5 表明它并不是真正的递归。当前扩展 (2x2) 的元数只包含最多 4 个可能的值,所以碰巧处理了我尝试过的初始情况。
const FIELD_NAMES = [
"height",
"width",
"depth",
"time",
"amaze",
] as const;
const SORT_ORDERS = [
"asc",
"desc",
] as const;
type Field = typeof FIELD_NAMES[number];
type Order = typeof SORT_ORDERS[number];
type FieldOrder = `${Field} ${Order}`
type Separated<S extends string> = `${S}${""|`, ${S}`}`;
type Sort = Separated<Separated<FieldOrder>>;
/** SUCCESS CASES */
const sort1:Sort = "height asc"; //compiles
const sort2:Sort = "height asc, depth desc"; //compiles
const sort3:Sort = "height asc, height asc, height asc"; //compiles
const sort4:Sort = "height asc, width asc, depth desc, time asc"; //compiles
const sort5:Sort = "height asc, width asc, depth desc, time asc, amaze desc"; //SHOULD compile but doesn't
/** FAILURE CASES */
const sort6:Sort = "height"; //doesn't compile
const sort7:Sort = "height asc,"; //doesn't compile
const sort8:Sort = ""; //doesn't compile
我不能再增加这个模板文字的 'arity',因为像下面那样尝试做 2x2x2 会导致 Expression produces a union type that is too complex to represent
type Sort = Separated<Separated<Separated<FieldOrder>>>;
是否可以定义一个模板文字来处理一般情况?
如你所见,template literal types you are creating quickly blow out the compiler's ability to represent unions. If you read the pull request that implements template literal types的那种,你会看到联合类型最多只能有100,000个元素。所以你只能让 Sort
接受最多 4 个逗号分隔值(这将需要大约 11,110 个成员)。而且您当然不能让它接受任意数字,因为那意味着 Sort
需要是一个无限并集,而无限大于 100,000。所以我们不得不放弃将 Sort
表示为特定联合类型的不可能完成的任务。
一般来说, in cases like this is to switch from specific types to generic types which act as recursive constraints。所以我们有 ValidSort<T>
而不是 Sort
。如果 T
是有效的排序字符串类型,则 ValidSort<T>
将等同于 T
。否则,ValidSort<T>
将是来自 Sort
的一些合理候选者(或这些的并集),它“接近” T
。
这意味着您打算编写 Sort
的任何地方现在都需要编写 ValidSort<T>
并将一些泛型类型参数添加到适当的范围。此外,除非你想强迫某人写 const s: ValidSort<"height asc"> = "height asc";
,否则你需要调用一个 辅助函数 ,类似于 asSort()
检查其输入并推断类型.意思是你得到 const s = asSort("height asc");
.
它可能并不完美,但它可能是我们能做的最好的。
让我们看看定义:
type ValidSort<T extends string> = T extends FieldOrder ? T :
T extends `${FieldOrder}, ${infer R}` ? T extends `${infer F}, ${R}` ?
`${F}, ${ValidSort<R>}` : never : FieldOrder;
const asSort = <T extends string>(t: T extends ValidSort<T> ? T : ValidSort<T>) => t;
ValidSort<T>
是一个 recursive conditional type,它检查字符串类型 T
以查看它是 FieldOrder
还是以 FieldOrder
开头的字符串后跟逗号和 space。如果它是 FieldOrder
,那么我们有一个有效的排序字符串,我们只是 return 它。如果它以 FieldOrder
开头,那么我们递归地检查字符串的其余部分。否则,我们有一个无效的排序字符串,我们 return FieldOrder
.
让我们看看实际效果。您的成功案例现在都按预期工作:
/** SUCCESS CASES */
const sort1 = asSort("height asc"); //compiles
const sort2 = asSort("height asc, depth desc"); //compiles
const sort3 = asSort("height asc, height asc, height asc"); //compiles
const sort4 = asSort("height asc, width asc, depth desc, time asc"); //compiles
const sort5 = asSort(
"height asc, width asc, depth desc, time asc, amaze desc"); //compiles
失败案例失败,错误消息显示“足够接近”的类型,您应该改用:
/** FAILURE CASES */
const sort6 = asSort("height"); // error!
/* Argument of type '"height"' is not assignable to parameter of type
'"height asc" | "height desc" | "width asc" | "width desc" | "depth asc" |
"depth desc" | "time asc" | "time desc" | "amaze asc" | "amaze desc"'. */
const sort7 = asSort("height asc,"); // error!
/* Argument of type '"height asc,"' is not assignable to parameter of type
'"height asc" | "height desc" | "width asc" | "width desc" | "depth asc" |
"depth desc" | "time asc" | "time desc" | "amaze asc" | "amaze desc"'. */
const sort8 = asSort(""); // error!
/* Argument of type '""' is not assignable to parameter of type
'"height asc" | "height desc" | "width asc" | "width desc" | "depth asc" |
"depth desc" | "time asc" | "time desc" | "amaze asc" | "amaze desc"'. */
const sort9 = asSort("height asc, death desc"); // error!
/* Argument of type '"height asc, death desc"' is not assignable to parameter of type
'"height asc, depth desc" | "height asc, height asc" | "height asc, time asc" |
"height asc, amaze desc" | "height asc, height desc" | "height asc, width asc" |
"height asc, width desc" | "height asc, depth asc" | "height asc, time desc" |
"height asc, amaze asc"'. */
我添加了 sort9
来向您展示错误消息如何不仅向您显示 FieldOrder
,还向您显示以 "height asc, "
开头后跟 FieldOrder
.[=44 的字符串=]
我正在尝试为包含逗号分隔值的字符串定义 Typescript 模板文字。我可以使这个定义真正递归和通用吗?
请参阅 this typescript playground 以试验案例。
每个逗号分隔值代表一个排序顺序,如 height asc
。该字符串应定义一个顺序(包括主要、次要、第三等),根据有效字段名称的并集和两个可能的顺序 "asc"
和 "desc"
,可以包含无限多的排序级别,由逗号按照示例代码中的示例。
下面的实现最多处理 4 个排序顺序,但案例 5 表明它并不是真正的递归。当前扩展 (2x2) 的元数只包含最多 4 个可能的值,所以碰巧处理了我尝试过的初始情况。
const FIELD_NAMES = [
"height",
"width",
"depth",
"time",
"amaze",
] as const;
const SORT_ORDERS = [
"asc",
"desc",
] as const;
type Field = typeof FIELD_NAMES[number];
type Order = typeof SORT_ORDERS[number];
type FieldOrder = `${Field} ${Order}`
type Separated<S extends string> = `${S}${""|`, ${S}`}`;
type Sort = Separated<Separated<FieldOrder>>;
/** SUCCESS CASES */
const sort1:Sort = "height asc"; //compiles
const sort2:Sort = "height asc, depth desc"; //compiles
const sort3:Sort = "height asc, height asc, height asc"; //compiles
const sort4:Sort = "height asc, width asc, depth desc, time asc"; //compiles
const sort5:Sort = "height asc, width asc, depth desc, time asc, amaze desc"; //SHOULD compile but doesn't
/** FAILURE CASES */
const sort6:Sort = "height"; //doesn't compile
const sort7:Sort = "height asc,"; //doesn't compile
const sort8:Sort = ""; //doesn't compile
我不能再增加这个模板文字的 'arity',因为像下面那样尝试做 2x2x2 会导致 Expression produces a union type that is too complex to represent
type Sort = Separated<Separated<Separated<FieldOrder>>>;
是否可以定义一个模板文字来处理一般情况?
如你所见,template literal types you are creating quickly blow out the compiler's ability to represent unions. If you read the pull request that implements template literal types的那种,你会看到联合类型最多只能有100,000个元素。所以你只能让 Sort
接受最多 4 个逗号分隔值(这将需要大约 11,110 个成员)。而且您当然不能让它接受任意数字,因为那意味着 Sort
需要是一个无限并集,而无限大于 100,000。所以我们不得不放弃将 Sort
表示为特定联合类型的不可能完成的任务。
一般来说,ValidSort<T>
而不是 Sort
。如果 T
是有效的排序字符串类型,则 ValidSort<T>
将等同于 T
。否则,ValidSort<T>
将是来自 Sort
的一些合理候选者(或这些的并集),它“接近” T
。
这意味着您打算编写 Sort
的任何地方现在都需要编写 ValidSort<T>
并将一些泛型类型参数添加到适当的范围。此外,除非你想强迫某人写 const s: ValidSort<"height asc"> = "height asc";
,否则你需要调用一个 辅助函数 ,类似于 asSort()
检查其输入并推断类型.意思是你得到 const s = asSort("height asc");
.
它可能并不完美,但它可能是我们能做的最好的。
让我们看看定义:
type ValidSort<T extends string> = T extends FieldOrder ? T :
T extends `${FieldOrder}, ${infer R}` ? T extends `${infer F}, ${R}` ?
`${F}, ${ValidSort<R>}` : never : FieldOrder;
const asSort = <T extends string>(t: T extends ValidSort<T> ? T : ValidSort<T>) => t;
ValidSort<T>
是一个 recursive conditional type,它检查字符串类型 T
以查看它是 FieldOrder
还是以 FieldOrder
开头的字符串后跟逗号和 space。如果它是 FieldOrder
,那么我们有一个有效的排序字符串,我们只是 return 它。如果它以 FieldOrder
开头,那么我们递归地检查字符串的其余部分。否则,我们有一个无效的排序字符串,我们 return FieldOrder
.
让我们看看实际效果。您的成功案例现在都按预期工作:
/** SUCCESS CASES */
const sort1 = asSort("height asc"); //compiles
const sort2 = asSort("height asc, depth desc"); //compiles
const sort3 = asSort("height asc, height asc, height asc"); //compiles
const sort4 = asSort("height asc, width asc, depth desc, time asc"); //compiles
const sort5 = asSort(
"height asc, width asc, depth desc, time asc, amaze desc"); //compiles
失败案例失败,错误消息显示“足够接近”的类型,您应该改用:
/** FAILURE CASES */
const sort6 = asSort("height"); // error!
/* Argument of type '"height"' is not assignable to parameter of type
'"height asc" | "height desc" | "width asc" | "width desc" | "depth asc" |
"depth desc" | "time asc" | "time desc" | "amaze asc" | "amaze desc"'. */
const sort7 = asSort("height asc,"); // error!
/* Argument of type '"height asc,"' is not assignable to parameter of type
'"height asc" | "height desc" | "width asc" | "width desc" | "depth asc" |
"depth desc" | "time asc" | "time desc" | "amaze asc" | "amaze desc"'. */
const sort8 = asSort(""); // error!
/* Argument of type '""' is not assignable to parameter of type
'"height asc" | "height desc" | "width asc" | "width desc" | "depth asc" |
"depth desc" | "time asc" | "time desc" | "amaze asc" | "amaze desc"'. */
const sort9 = asSort("height asc, death desc"); // error!
/* Argument of type '"height asc, death desc"' is not assignable to parameter of type
'"height asc, depth desc" | "height asc, height asc" | "height asc, time asc" |
"height asc, amaze desc" | "height asc, height desc" | "height asc, width asc" |
"height asc, width desc" | "height asc, depth asc" | "height asc, time desc" |
"height asc, amaze asc"'. */
我添加了 sort9
来向您展示错误消息如何不仅向您显示 FieldOrder
,还向您显示以 "height asc, "
开头后跟 FieldOrder
.[=44 的字符串=]