强制数组对于联合类型是详尽的

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

给定一个使用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
3个回答
19
投票

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;

Playground 代码链接


0
投票

我还有其他建议

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 中删除第一个元素

游乐场


0
投票

我使用的另一个技巧是利用

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[]
© www.soinside.com 2019 - 2024. All rights reserved.