【问题标题】:Typescript - Create an exhaustive Tuple type from another typeTypescript - 从另一种类型创建详尽的元组类型
【发布时间】:2020-01-29 17:34:09
【问题描述】:

我正在尝试在 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]>>[]
}

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

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

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

我想让 typescript 将ActionGroup1 解析为

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

这可能吗?

【问题讨论】:

    标签: typescript


    【解决方案1】:

    如果您想要求 namecall 属性匹配(因此您不能使用“不希望的名称/功能组合”),您可以这样做:

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

    基本上,您正在迭代 T 的每个属性 K 的所有子属性 P,然后将 {name: P; call: T[K][P]} 联合在一起。所以你最终得到Array&lt;{name: "Action1", call: ()=&gt;number} | {name: "Action2", call: (x: number)=&gt;string}&gt;

    对于ActionInterfaceArray&lt;MyActionInterface&gt;,你得到

    {
        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: ()=&gt;number}, {name: "Action2", call: (x: number)=&gt;string}] 这样的元组类型,因为原来的ActionGroup1 是一个对象类型,而对象类型没有有序键。所以语言中没有任何内容会说“将Action1放在第一位,Action2放在第二位”,这甚至不是您想要做的,是吗?

    您可以潜在地表示所有可能的元组类型的联合,例如[{name: "Action1", call: ()=&gt;number}, {name: "Action2", call: (x: number)=&gt;string}] | [{name: "Action2", call: (x: number)=&gt;string}, {name: "Action1", call: ()=&gt;number}],但这既是编程生成的真正痛苦,也会导致无法管理的元组组合爆炸,即使是数量适中的术语。 (例如,将Array&lt;A | B | C | D&gt; 变成[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&lt;T&gt; 的自然实现是以 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&lt;MyActionInterface&gt;,你会得到:

    {
        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&lt;T, U&gt;,它尝试验证候选类型U 是否具有与T 的子属性相对应的所有必需属性。它基于ActionInterfaceArray&lt;T&gt;,但如果传入的数组中的任何一个缺少键(keyof T[K] extends U[K][number]["name"] 为假),那么您将使该类型需要缺少的键。错误不是很大,而且很难阅读,但这里是:

    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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2022-01-10
      • 1970-01-01
      • 1970-01-01
      • 2018-06-12
      • 2021-09-27
      • 2021-07-02
      • 2013-03-23
      相关资源
      最近更新 更多