强制数组在联合类型上穷举

时间:2019-03-20 16:26:40

标签: typescript

给出一个使用诸如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')

2 个答案:

答案 0 :(得分:3)

有多种方法可以这样做,但对您来说可能有点混乱。这里的两个绊脚石是缺少partial type parameter inferenceinvalid types。这是我的解决方案:

type Furniture = 'chair' | 'table' | 'lamp' | 'ottoman';

const exhaustiveStringTuple = <T extends string>() =>
  <L extends T[]>(
    ...x: L & ([T] extends [L[number]] ? L : [
      Error, "You are missing ", Exclude<T, L[number]>])
  ) => x;

const missingFurniture = exhaustiveStringTuple<Furniture>()('chair', 'table', 'lamp');
// error, [string, string, string] is not assignable to parameter of type
// ["chair", "table", "lamp"] & [Error, "You are missing", "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 Furniture[]>(
    ...x: L & ([Furniture] extends [L[number]] ? L : [
    Error, "You are missing ", Exclude<Furniture, L[number]>])
  ) => x;

const missingFurniture = furnitureTuple('chair', 'table', 'lamp');
// error, [string, string, string] is not assignable to parameter of type
// ["chair", "table", "lamp"] & [Error, "You are missing", "ottoman"]

const extraFurniture = furnitureTuple('chair', 'table', 'lamp', 'ottoman', 'bidet');
// error, "bidet" is not assignable to a parameter of type 'Furniture'

const furniture = furnitureTuple('chair', 'table', 'lamp', 'ottoman');
// okay

另一个问题是,省略必需的参数时出现的错误是。我只是做了一些垃圾,希望开发人员可以解释([Error, "You are missing ", "ottoman"],即使从类型上来讲这毫无意义。如果我们的类型无效,则可以自定义错误消息。

那是我能做的最好的。它的行为符合您的要求,但是正如我所说的,它可能比您想要的更混乱。希望能有所帮助;祝你好运!

答案 1 :(得分: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

从元组中删除将获取元组并返回不包含第一个元素的元组(稍后将需要)

检查将在元组上迭代。当Tuple [0]不扩展Union时,每个步骤都可以返回never,在输入tuple为空时退出,并在Tuple [0]扩展Union时返回下一步。在下一步中,我们递归调用Check,但是首先我们使用先前的util

从Tuple中删除了第一个元素

Playground