打字稿-从另一种类型创建穷举的元组类型

时间:2019-09-30 12:07:26

标签: typescript

我正在尝试在打字稿中创建一个伪接口。所需的行为如下所示:

我想创建一个包含任意数量的动作组的类型,其中包含任意数量的动作

type MyActionInterface = { 
    ActionGroup1: { 
        Action1: () => number, 
        Action2: (x:number) => string 
    },
    ActionGroup2: {
        Action1: (x: number, y:number) => void, 
        Action2: (x:string) => number
    }
}

然后可以用来描述我具体的实现对象

const someInterfacedObject:ActionInterface<MyActionInterface> = {
    ActionGroup1: [
        {
            name: 'Action1',
            call: () => 3
        },
        {
            name: 'Action2',
            call: (x) => "X"+x
        }
    ],
    ActionGroup2: [
        {
            name: 'Action1',
            call: (x, y) => { }
        },
        {
            name: 'Action2',
            call: (x) => 3
        }
    ]
}

当前代码看起来像这样,并且可以正常工作

type ValueOf<T> = T[keyof T];
type AnyFunction = (...args:any) => any

type Action<N, F> = {
    name: N
    call: F
}   

type ActionInterface<Definition> = 
{ 
    [groupname in keyof Definition] : Action<keyof Definition[groupname], ValueOf<Definition[groupname]>>[]
}

现在,ActionGroup1中的someInterfacedObject的类型解析为

ActionGroup1: Action<"Action1" | "Action2", (() => number) | ((x: number) => string)>[]

这仍然允许我省略操作或输入名称/功能的不想要的组合

我想获取打字稿以将ActionGroup1解析为

[Action<"Action1", () => number>, Action<"Action2", (x:number) => string>]

这可能吗?

1 个答案:

答案 0 :(得分:0)

如果您想要求namecall属性匹配(这样就不能使用“名称/功能的不必要组合”),您可以这样操作:< / p>

type ActionInterfaceArray<T> = {
  [K in keyof T]: Array<
    { [P in keyof T[K]]: { name: P; call: T[K][P] } }[keyof T[K]]
  >
};

基本上,您要遍历P的每个属性K的所有子属性T,然后将{name: P; call: T[K][P]}合并在一起。因此,您最终得到Array<{name: "Action1", call: ()=>number} | {name: "Action2", call: (x: number)=>string}>

对于ActionInterfaceArray<MyActionInterface>,您可以获得

{
    ActionGroup1: ({
        name: "Action1";
        call: () => number;
    } | {
        name: "Action2";
        call: (x: number) => string;
    })[];
    ActionGroup2: ({
        name: "Action1";
        call: (x: number, y: number) => void;
    } | {
        name: "Action2";
        call: (x: string) => number;
    })[];
}

到目前为止,这是最简单的方法。


不幸的是,它不能解决“允许我省略操作”问题。您不能真正输出像[{name: "Action1", call: ()=>number}, {name: "Action2", call: (x: number)=>string}]这样的元组类型,因为原始的ActionGroup1是对象类型,并且对象类型没有排序键。因此,该语言中没有任何内容会说“将Action1放在第一位,并将Action2放在第二位”,这甚至不是您要执行的操作,对吗?

您可以潜在地表示所有可能的元组类型的并集,例如[{name: "Action1", call: ()=>number}, {name: "Action2", call: (x: number)=>string}] | [{name: "Action2", call: (x: number)=>string}, {name: "Action1", call: ()=>number}],但这既很难以编程方式生成,又会导致难以处理的元组组合爆炸甚至数量不多的术语。 (例如,将Array<A | B | C | D>变成[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])我不推荐这样做,但这是一种接近的方法(UnionToAllPossibleTuples<T>的自然实现是TypeScript编译器不具有的递归方式)很喜欢,因此我将其展开为可以处理最多七个成员的工会的东西):

type UnionToAllPossibleTuples<T> = UTAPT<T>;
type UTAPT<T, U = T> = [T] extends [never] ? [] : T extends any ? Cons<T, UTAPT1<Exclude<U, T>>> : never;
type UTAPT1<T, U = T> = [T] extends [never] ? [] : T extends any ? Cons<T, UTAPT2<Exclude<U, T>>> : never;
type UTAPT2<T, U = T> = [T] extends [never] ? [] : T extends any ? Cons<T, UTAPT3<Exclude<U, T>>> : never;
type UTAPT3<T, U = T> = [T] extends [never] ? [] : T extends any ? Cons<T, UTAPT4<Exclude<U, T>>> : never;
type UTAPT4<T, U = T> = [T] extends [never] ? [] : T extends any ? Cons<T, UTAPT5<Exclude<U, T>>> : never;
type UTAPT5<T, U = T> = [T] extends [never] ? [] : T extends any ? Cons<T, UTAPT6<Exclude<U, T>>> : never;
type UTAPT6<T, U = T> = [T] extends [never] ? [] : T extends any ? Cons<T, UTAPT7<Exclude<U, T>>> : never;
type UTAPT7<T, U = T> = [];

type Cons<T, U = []> = U extends any[]
  ? ((t: T, ...u: U) => void) extends ((...r: infer R) => void) ? R : never
  : [T];

然后

type ActionInterfaceTuples<T> = {
  [K in keyof T]: UnionToAllPossibleTuples<
    { [P in keyof T[K]]: { name: P; call: T[K][P] } }[keyof T[K]]
  >
};

对于ActionInterfaceTuples<MyActionInterface>,您将获得:

{
    ActionGroup1: [{
        name: "Action1";
        call: () => number;
    }, {
        name: "Action2";
        call: (x: number) => string;
    }] | [{
        name: "Action2";
        call: (x: number) => string;
    }, {
        name: "Action1";
        call: () => number;
    }];
    ActionGroup2: [{
       name: "Action1";
        call: (x: number, y: number) => void;
    }, {
        name: "Action2";
        call: (x: string) => number;
    }] | [{
        name: "Action2";
        call: (x: string) => number;
    }, {
        name: "Action1";
        call: (x: number, y: number) => void;
    }];
}

在这里效果很好,但是...好吧。


您可以做的另一件事是使通用类型ActionInterfaceGeneric<T, U>试图验证候选类型U具有与T的子属性相对应的所有必需属性。它基于ActionInterfaceArray<T>,但是如果任何传入的数组都缺少键(keyof T[K] extends U[K][number]["name"]为false),那么您将使该类型要求缺少键。错误不是很大,很难读取,但是这里是:

type ActionInterfaceGeneric<T, U extends ActionInterfaceArray<T>> = {
  [K in keyof T]: keyof T[K] extends U[K][number]["name"]
    ? U[K]
    : [{ name: Exclude<keyof T[K], U[K][number]["name"]>; call: any }]
};

const asActionInterface = <T>() => <
  U extends ActionInterfaceArray<T> & ActionInterfaceGeneric<T, U>
>(
  u: U
) => u;

这将适合您的正确值:

const someInterfacedObject = asActionInterface<MyActionInterface>()({
  ActionGroup1: [
    {
      name: "Action1",
      call: () => 3
    },
    {
      name: "Action2",
      call: x => "X" + x
    }
  ],
  ActionGroup2: [
    {
      name: "Action1",
      call: (x, y) => {}
    },
    {
      name: "Action2",
      call: () => 3
    }
  ]
});

但是对于不正确的值,您会得到一些错误:

const badInterfacedObject = asActionInterface<MyActionInterface>()({
  ActionGroup1: [
    {
      name: "Action1",
      call: () => 3
    },
    {
      name: "Action2",
      call: x => "X" + x
    }
  ],
  ActionGroup2: [    
    {
      name: "Action2",  // error! '"Action2"' is not assignable to type '"Action1"'.
      call: () => 3
    }
  ]
});

该错误"Action2" is not assignable to "Action1"有点令人困惑,特别是因为如果将其更改为"Action1",它将把错误切换为"Action2"。确实,您希望该错误说出诸如“嘿,您的数组不够长”之类的内容,但是很难获得custom error messages。上面的内容足够接近,可以演示通用方法。


因此,ActionInterfaceGenericActionInterfaceTuples都以其自己的方式很复杂,并且ActionInterfaceArray是不完整的……这些问题表明TypeScript的类型系统并不太适合此那类的东西。如果您想与TypeScript一起很好地玩,我建议您放弃需要完全匹配的对象的数组,而只使用对象本身。当然,这可能对您不可行。如果是这样,我可能会只选择ActionInterfaceArray并使用一些著名的文档,使用它们时一定要小心。但这取决于您使用哪个(如果有)。

好的,希望能有所帮助;祝你好运!

Link to code