类型是对象所有键的串联

问题描述 投票:0回答:1

假设我有这种类型:

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';

如何进行?我知道如何使用嵌套属性做这样的事情,但我无法掌握这个。

typescript
1个回答
0
投票

警告 这只是为了探索类型系统,而不是您应该在实际生产系统中使用的东西。您要求获取对象类型中键的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" */
看起来不错,最后一次:它的伸缩性

非常糟糕

Playground 代码链接

© www.soinside.com 2019 - 2024. All rights reserved.