我正在尝试在打字稿中创建一个伪接口。所需的行为如下所示:
我想创建一个包含任意数量的动作组的类型,其中包含任意数量的动作
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>]
这可能吗?
答案 0 :(得分:0)
如果您想要求name
和call
属性匹配(这样就不能使用“名称/功能的不必要组合”),您可以这样操作:< / 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。上面的内容足够接近,可以演示通用方法。
因此,ActionInterfaceGeneric
和ActionInterfaceTuples
都以其自己的方式很复杂,并且ActionInterfaceArray
是不完整的……这些问题表明TypeScript的类型系统并不太适合此那类的东西。如果您想与TypeScript一起很好地玩,我建议您放弃需要完全匹配的对象的数组,而只使用对象本身。当然,这可能对您不可行。如果是这样,我可能会只选择ActionInterfaceArray
并使用一些著名的文档,使用它们时一定要小心。但这取决于您使用哪个(如果有)。
好的,希望能有所帮助;祝你好运!