如果您想要求 name 和 call 属性匹配(因此您不能使用“不希望的名称/功能组合”),您可以这样做:
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<{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"] 为假),那么您将使该类型需要缺少的键。错误不是很大,而且很难阅读,但这里是:
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 并使用一些在使用时必须小心的重要文档。但这取决于您使用哪一个(如果有的话)。
好的,希望对您有所帮助;祝你好运!
Link to code