Typescript - 从另一种类型创建一个详尽的元组类型 [英] Typescript - Create an exhaustive Tuple type from another type

查看:19
本文介绍了Typescript - 从另一种类型创建一个详尽的元组类型的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试在打字稿中创建一个伪接口.所需的行为看起来像这样:

I'm trying to create a pseudo-interface in typescript. The desired behavior would look something like this:

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

I'd like to create a type containing any number of action groups which contain any number of actions

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

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

Which can then be used to describe my concrete implementation object

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
        }
    ]
}

代码目前看起来像这样并且有点作用

The code currently looks like this and works somewhat

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 的类型解析为

Now the type of, for example, ActionGroup1 in someInterfacedObject resolves as

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

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

which does still allow me to omit actions or input undesired combinations of name/function

我想让打字稿将 ActionGroup1 解析为

I'd like to get typescript to resolve ActionGroup1 as

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

这可能吗?

推荐答案

如果你想要求 namecall 属性匹配(这样你就可以t 使用不需要的名称/功能组合"),你可以这样做:

If you want to require that the name and the call properties match up (so you can't use "undesired combinations of name/function"), you can do it like this:

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;调用:T[K][P]}.所以你最终得到 Array<{name: "Action1", call: ()=>number} |{name: "Action2", call: (x: number)=>string}>.

Basically you're iterating over all the subproperties P of each property K of T and then unioning together the {name: P; call: T[K][P]}. So you end up with Array<{name: "Action1", call: ()=>number} | {name: "Action2", call: (x: number)=>string}>.

对于ActionInterfaceArray,你得到

{
    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;
    })[];
}

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

This is the simplest approach, by far.

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

Unfortunately it doesn't solve the "allow me to omit actions" problem. You can't really output a tuple type like [{name: "Action1", call: ()=>number}, {name: "Action2", call: (x: number)=>string}], since the original ActionGroup1 is an object type, and object types don't have ordered keys. So nothing in the language will say "place Action1 first and Action2 second", and that's not even what you're trying to do, is it?

您可以潜在地表示所有可能的元组类型的联合,例如 [{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, 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 的自然实现是以 TypeScript 编译器不喜欢的方式递归,所以我将它展开成可以处理多达七个成员的联合的东西):

You could potentially represent a union of all possible tuple types, such as [{name: "Action1", call: ()=>number}, {name: "Action2", call: (x: number)=>string}] | [{name: "Action2", call: (x: number)=>string}, {name: "Action1", call: ()=>number}], but this is both a real pain to generate programmatically and results in an unmanageable combinatorial explosion of tuples for even a modest number of terms. (e.g., turning Array<A | B | C | D> into [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]) I wouldn't recommend this, but here's one way to get close (The natural implementation of UnionToAllPossibleTuples<T> is recursive in a way the TypeScript compiler doesn't like, so I've unrolled it into something that will deal with unions of up to seven members):

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,你得到:

{
    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;
    }];
}

在这里工作得很好,但是......糟糕.

which works well enough here, but... yuck.

您可以做的另一件事是创建一个泛型类型 ActionInterfaceGeneric,它尝试验证候选类型 U 是否具有与T 的子属性.它基于 ActionInterfaceArray,但如果任何传入的数组缺少键 (keyof T[K] extends U[K][number]["name"] 为假),那么您将使该类型需要缺少的键.错误不是很大,也很难阅读,但这里是:

Another thing you could do is make a generic type ActionInterfaceGeneric<T, U> that tries to verify that a candidate type U has all the required properties corresponding to the subproperties of T. It's based on ActionInterfaceArray<T>, but if any of the passed in arrays are missing keys (keyof T[K] extends U[K][number]["name"] is false), then you will make the type require the missing key. The errors aren't great, and it's hard to read, but here it is:

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;

这将适用于您的正确价值:

That will work for your correct value:

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
    }
  ]
});

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

but for an incorrect value you'll get some errors:

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 notassignable to "Action1" 有点令人困惑,特别是因为如果你把它改成 "Action1" 它会切换错误Action2".你真的希望错误说嘿你的数组不够长",但很难得到 自定义错误消息.以上足以说明一般方法.

That error, "Action2" is not assignable to "Action1" is a bit confusing, especially since if you change it to "Action1" it will switch the error to "Action2". Really you want the error to say something like "hey your array isn't long enough", but it's hard to get custom error messages. The above is close enough to demonstrate the general approach.

所以,ActionInterfaceGenericActionInterfaceTuples 各有各的复杂,ActionInterfaceArray 不完整……这些问题说明 TypeScript 的类型系统并不是真的很适合这种事情.如果您想很好地使用 TypeScript,我建议您放弃需要完全匹配对象的数组,而只使用对象本身.当然,这对您来说可能不可行.如果是这样,我可能只使用 ActionInterfaceArray 并使用一些突出的文档,使用时必须小心.但这取决于您要使用其中的哪一个(如果有).

So, both ActionInterfaceGeneric and ActionInterfaceTuples are complicated in their own ways, and ActionInterfaceArray is incomplete... these problems indicate that TypeScript's type system isn't really quite geared for this sort of thing. If you wanted to play nicely with TypeScript, I'd suggest ditching arrays-which-need-to-exactly-match-objects and just use the objects themselves. This might not be feasible for you, of course. If so, I'd probably just go with ActionInterfaceArray and use some prominent documentation that care must be taken when using it. But it's up to you which (if any) of these to use.

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

Okay, hope that helps; good luck!

链接到代码

这篇关于Typescript - 从另一种类型创建一个详尽的元组类型的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆