我将定义这个:
type Arg<T> = T extends any ? { arg: T } : never;
这样我们就可以使用Arg<E>
(相当于{arg:"el1"}|{arg:"el2"}|{arg:"el3"}
)如下。
你在这里能期望的最好的就是一些generic辅助函数verifyArray()
这将强制执行其参数的限制:
- 联合中的元素数组
- 联合体中不缺少任何元素
- 并且不包含重复项
而且它会很丑。
没有usable具体类型将对包含超过六个元素的联合强制执行此操作。可以使用一些非法递归或合法非递归但繁琐的类型定义来采用联合类型,例如0 | 1 | 2 | 3
并将其变成所有可能的联合tuples符合您的标准。这会产生类似的东西
type AllTuples0123 = UnionToAllPossibleTuples<0 | 1 | 2 | 3>
这相当于
type AllTuples0123 =
| [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]
但对于输入并集n将产生输出并集的元素n!(那是n 阶乘)产出,增长非常快n。对于你的例子"el1"|"el2"|"el3"
那就可以了:
type AllPossibleTuplesOfArgs = UnionToAllPossibleTuples<Arg<E>>;
const okay: AllPossibleTuplesOfArgs =
[{ "arg": "el1" }, { "arg": "el2" }, { "arg": "el3" }]; // okay
const bad1: AllPossibleTuplesOfArgs = [{ "arg": "el1" }, { "arg": "el2" }]; // error!
const bad2: AllPossibleTuplesOfArgs = // error!
[{ "arg": "el1" }, { "arg": "el2" }, { "arg": "el2" }, { "arg": "el3" }];
const bad3: AllPossibleTuplesOfArgs =
[{ "arg": "el1" }, { "arg": "el2" }, { "arg": "el8" }] // error!
但我假设当您的对象具有七个或更多属性时,您想要的东西不会使编译器崩溃。所以让我们放弃吧UnionToAllPossibleTuples
和任何具体类型。
那么什么会verifyArray()
看起来像?
首先让我们创建一个名为的类型函数NoRepeats<T>
它采用元组类型T
并返回相同的东西T
当且仅当T
没有重复的元素...否则它返回一个修改后的元组T
是不可分配的。这将使我们能够做出约束T extends NoRepeats<T>
说“元组类型T
没有重复的元素”。这是一种方法:
type NoRepeats<T extends readonly any[]> = { [M in keyof T]: { [N in keyof T]:
N extends M ? never : T[M] extends T[N] ? unknown : never
}[number] extends never ? T[M] : never }
So NoRepeats<[0,1,2]>
is [0,1,2]
, but NoRepeats<[0,1,1]>
is [0,never,never]
. Then verifyArray()
可以写成这样:
const verifyArray = <T>() => <U extends NoRepeats<U> & readonly T[]>(
u: (U | [never]) & ([T] extends [U[number]] ? unknown : never)
) => u;
它需要一个类型T
检查,并返回一个新函数,确保其参数没有重复(来自U extends NoRepeats<U>
),可分配给T[]
(from & readonly T
),并且不遗漏任何元素T
(from & ([T] extends [U[number]] ? unknown : never)
)。是的,它很丑。让我们看看它是否有效:
const verifyArgEArray = verifyArray<Arg<E>>()
const okayGeneric = verifyArgEArray([{ "arg": "el3" }, { "arg": "el1" }, { "arg": "el2" }]); // okay
const bad1Generic = verifyArgEArray([{ "arg": "el1" }, { "arg": "el2" }]); // error
const bad2Generic = // error
verifyArgEArray([{ "arg": "el1" }, { "arg": "el2" }, { "arg": "el2" }, { "arg": "el3" }]);
const bad3Generic = // error
verifyArgEArray([{ "arg": "el1" }, { "arg": "el2" }, { "arg": "el8" }]);
这样就可以了。
这两者都迫使你与类型系统作斗争。您可以创建一个构建器类,如下所示这个答案它与类型系统配合得更好,但涉及更多的运行时开销,并且可以说只是稍微不那么难看。
老实说,我建议尝试重构您的代码,而不需要 TypeScript 来强制执行此操作。最简单的事情是要求一个对象将这些值作为键(例如,只需创建一个类型的值T
或者可能Record<keyof T, any>
)并使用它代替(或在生成之前)数组。哦,好吧,希望这有帮助。祝你好运!
Playground 代码链接