给定一个使用here所描述的技术创建的强类型元组:
const tuple = <T extends string[]>(...args: T) => args;
const furniture = tuple('chair', 'table', 'lamp');
// typeof furniture[number] === 'chair' | 'table' | 'lamp'
我想在设计时断言它比另一种联合类型更详尽:
type Furniture = 'chair' | 'table' | 'lamp' | 'ottoman'
如何创建一个类型来确保
furniture
包含且仅包含 Furniture
联合中的每个类型?
目标是能够像这样在设计时创建一个数组,并且如果
Furniture
发生变化,它就会失败;理想的语法可能如下所示:
const furniture = tuple<Furniture>('chair', 'table', 'lamp')
TypeScript 并没有真正直接支持“穷举数组”。 您可以引导编译器检查这一点,但这对您来说可能有点混乱。 一个绊脚石是缺乏部分类型参数推断(如microsoft/TypeScript#26242中的要求)。 这是我的解决方案:
type Furniture = 'chair' | 'table' | 'lamp' | 'ottoman';
type AtLeastOne<T> = [T, ...T[]];
const exhaustiveStringTuple = <T extends string>() =>
<L extends AtLeastOne<T>>(
...x: L extends any ? (
Exclude<T, L[number]> extends never ?
L :
Exclude<T, L[number]>[]
) : never
) => x;
const missingFurniture = exhaustiveStringTuple<Furniture>()('chair', 'table', 'lamp');
// error, Argument of type '"chair"' is not assignable to parameter of type '"ottoman"'
const extraFurniture = exhaustiveStringTuple<Furniture>()(
'chair', 'table', 'lamp', 'ottoman', 'bidet');
// error, "bidet" is not assignable to a parameter of type 'Furniture'
const furniture = exhaustiveStringTuple<Furniture>()('chair', 'table', 'lamp', 'ottoman');
// okay
如您所见,
exhaustiveStringTuple
是一个curried函数,其唯一目的是采用手动指定的类型参数T
,然后返回一个新函数,该函数采用其类型受T
约束但由下式推断的参数的电话。 (如果我们有适当的部分类型参数推断,则可以消除柯里化。)在您的情况下,T
将被指定为Furniture
。 如果您只关心exhaustiveStringTuple<Furniture>()
,那么您可以使用它:
const furnitureTuple =
<L extends AtLeastOne<Furniture>>(
...x: L extends any ? (
Exclude<Furniture, L[number]> extends never ? L : Exclude<Furniture, L[number]>[]
) : never
) => x;
我还有其他建议
type RemoveFirstFromTuple<T extends any[]> =
T extends [] ? undefined :
(((...b: T) => void) extends (a: any, ...b: infer I) => void ? I : [])
const tuple = <T extends string[]>(...args: T) => args;
type FurnitureUnion = 'chair' | 'table' | 'lamp';
type FurnitureTuple = ['chair', 'table' , 'lamp'];
type Check<Union, Tuple extends Array<any>> = {
"error": never,
"next": Check<Union, RemoveFirstFromTuple<Tuple>>,
"exit": true,
}[Tuple extends [] ? "exit" : Tuple[0] extends Union ? "next" : "error"];
type R = Check<FurnitureUnion, FurnitureTuple>; // true
type R1 = Check<'chair' | 'lamp' | 'table', FurnitureTuple>; // true
type R2 = Check<'chair' | 'lamp' | 'table', ['chair', 'table' , 'lamp', 'error']>; // nerver
从元组中删除需要元组并返回没有第一个元素的元组(稍后将需要)
Check 将迭代 Tuple。当Tuple[0]不扩展Union时,每一步都可以返回
never
,当输入元组为空时退出,当Tuple[0]扩展Union时,下一步。在下一步中,我们递归调用 Check,但首先我们通过之前的 util 从 Tuple 中删除第一个元素
我使用的另一个技巧是利用
Record
类型来强制定义所有键,我使用 true
作为“无操作”值。 TypeScript 强制所有键都存在,从而提供类型安全。然后,您可以根据需要构建一个键列表。
这可能并不严格等同于类型检查,并且比直接列出数组中的值更冗长。
type WithFourLegs = 'Cat' | 'Cow' | 'Dog';
type WithTwoLegs = 'Human' | 'Ostrich';
// In my control
type WithLegs = WithFourLegs | WithTwoLegs;
// Should fail because 'Dog' and 'Ostrich' are missing
const allWithLegsMap: Record<WithLegs, true> = {
Cat: true,
Cow: true,
Dog: true,
Human: true,
Ostrich: true
}
const allWithLegs = Object.keys(allWithLegsMap) as WithLegs[]