假设我有这种类型:
type MyObject = {
one: string;
two: string;
};
我想创建一个像这样的类型:
type Concatenator<T extends Record<string, string>, S = `/`> = `TODO HERE`;
这会产生以下可接受的值:
// I don't care about the order, so either one is fine, or both is fine too
const concatenated: Concatenator<MyObject> = 'one/two';
const concatenated: Concatenator<MyObject> = 'two/one';
如何进行?我知道如何使用嵌套属性做这样的事情,但我无法掌握这个。
警告 这只是为了探索类型系统,而不是您应该在实际生产系统中使用的东西。您要求获取对象类型中键的union(通过
keyof
运算符)并以每种可能的顺序连接它们。这随着键的数量而扩展得非常糟糕,因为排列的数量呈超指数增长。因此,对于三个键来说这很好,对于五个键来说很难阅读,对于七个键来说在计算机上很难阅读,而对于九个键来说则不可能。
好吧,让我们把它分成几部分。
给定一个联合类型
T
,让我们编写一个 generic AllPermutations<T>
实用程序类型,其计算结果为联合成员每种可能排序的 tuples 的联合:
type AllPermutations<T, U = T> =
T extends any ? [U] extends [T] ? [T] : [T, ...AllPermutations<Exclude<U, T>>] : never;
这是 T
上的
分配条件类型,因此这意味着
T
被拆分为其联合成员,并且在将结果连接在一起之前单独评估每个成员。 U
类型参数实际上是完整联合 T
的副本,它不 被拆分为成员。所以 U
是完整联盟,而 T
是联盟的每个成员。 如果 [U] extends [T]
为 true,这是因为联合 U
有一个成员([
+]
是为了防止在 U
中的联合上进行分配),所以只有一种排列,即一元组 [T]
。 否则,我们递归,并将 T
添加到 AllPermutations<Exclude<U, T>>
的结果中。也就是说,我们从并集 T
中删除 U
并得到所有这些排列。
让我们测试一下:
type KeyPerms = AllPermutations<keyof MyObject>;
// type KeyPerms = ["one", "two"] | ["two", "one"]
type ExamplePerms = AllPermutations<0 | 1 | 2 | 3>;
/* type ExamplePerms =
[0, 1, 2, 3] | [0, 1, 3, 2] | [0, 2, 1, 3] | [0, 2, 3, 1] | [0, 3, 1, 2] |
[0, 3, 2, 1] | [1, 0, 2, 3] | [1, 0, 3, 2] | [1, 2, 0, 3] | [1, 2, 3, 0] |
[1, 3, 0, 2] | [1, 3, 2, 0] | [2, 0, 1, 3] | [2, 0, 3, 1] | [2, 1, 0, 3] |
[2, 1, 3, 0] | [2, 3, 0, 1] | [2, 3, 1, 0] | [3, 0, 1, 2] | [3, 0, 2, 1] |
[3, 1, 0, 2] | [3, 1, 2, 0] | [3, 2, 0, 1] | [3, 2, 1, 0] */
看起来不错(但你可以看到它的扩展有多糟糕)。
现在,让我们编写一个实用程序类型
Join<T, S>
,其中T
是字符串文字类型的元组,S
是字符串文字分隔符:
type Join<T extends string[], S extends string, A extends string = never> =
T extends [infer F extends string, ...infer R extends string[]] ?
Join<R, S, [A] extends [never] ? F : `${A}${S}${F}`> : A
这也是一种分配条件类型,当
T
是并集时会有帮助,因此结果也将是并集。 基本上,我们使用可变元组类型将T
拆分为第一个元素F
和元组的其余部分R
,并递归(使用累加器类型参数A
,以便类型为a尾递归条件类型)。如果 T
为空,我们就达到了基本情况,只返回累加器 A
。否则,我们需要将 F
附加到 A
并递归。从概念上讲,这是 `${A}${S}${F}`
,但如果累加器 A
为空,那么我们将在开始时得到一个分隔符。 因此,我们必须进行额外检查 A
是否为空(我将其设为 never
)为 [A] extends [never] ? F `${A}${S}${F}`
。
让我们测试一下:
type JoinEx = Join<["a", "b", "c", "d"], "-">;
// type JoinEx = "a-b-c-d"
type JoinEx2 = Join<KeyPerms, "/">;
// type JoinEx2 = "one/two" | "two/one"
看起来也不错。
最后我们可以用
Concatenator
和 AllPermutations
制作 Join
:
type Concatenator<T extends Record<string, string>, S extends string = `/`> =
AllPermutations<keyof T> extends infer AP extends string[] ?
Join<AP, S> : never
从概念上讲,你只需编写
Join<AllPermutations<keyof T>, S>
,但 TypeScript 试图以一种阻止其工作的方式来简化它。我使用条件类型推断将AllPermutations<keyof T>
“复制”到新的类型参数AP
中(并将其约束为string[]
,以便
Join
接受它)。然后
Join<AP, S>
就可以了。最后我们来测试一下:
type MyObject = {
one: string;
two: string;
};
type Z = Concatenator<MyObject>
// type Z = "one/two" | "two/one"
let concatenated: Concatenator<MyObject>;
concatenated = 'one/two';
concatenated = 'two/one';
type W = Concatenator<{ a: '0', b: '1', c: '2', d: '3' }>;
/* type W = "a/b/c/d" | "a/b/d/c" | "a/c/b/d" | "a/c/d/b" | "a/d/b/c" | "a/d/c/b" | "b/a/c/d" |
"b/a/d/c" | "b/c/a/d" | "b/c/d/a" | "b/d/a/c" | "b/d/c/a" | "c/a/b/d" | "c/a/d/b" | "c/b/a/d" |
"c/b/d/a" | "c/d/a/b" | "c/d/b/a" | "d/a/b/c" | "d/a/c/b" | "d/b/a/c" | "d/b/c/a" | "d/c/a/b" |
"d/c/b/a" */
看起来不错,最后一次:它的伸缩性非常糟糕。