我有一个方法:
const invokeActions = (target, actions) => {
actions.forEach(({ action, args }) => {
target[action](...args);
});
}
如何正确键入此方法,以便编译器显示被调用函数的参数数量和参数类型错误的错误?
使用示例:
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const actions = [
{ action: 'moveTo', args: [0, 0] },
{ action: 'lineTo', args: [100, 100] },
{ action: 'closePath', args: ['error'] }, // Should return a TypeScript compiler error
{ action: 'closePath', args: [1, 2] }, // Same thing
];
invokeActions(ctx, actions);
我第一次尝试通过 interface
输入但是closePath
拿了[]
还有[number, number]
,这是不正确的。
interface Drawable {
moveTo: (x: number, y: number) => void;
lineTo: (x: number, y: number) => void;
closePath: () => void;
}
type ActionType = {
action: Drawable[keyof Drawable] extends (...args: any[]) => void ? keyof Drawable : never;
args: Drawable[keyof Drawable] extends (...args: any[]) => void ? Drawable[keyof Drawable] : never;
}
我还使用 infer
尝试了许多组合在(...args: infer Args)
,试图使 args: Parameters<T[keyof T]>
但 TypeScript 做了同样的事情或者只是显示错误。
最佳答案
为了让调用方能够正常工作,invokeActions()
的类型需要如下所示:
declare const invokeActions:
(target: Drawable, actions: Action[]) => void
type Drawable = {
moveTo: (x: number, y: number) => void;
lineTo: (x: number, y: number) => void;
closePath: () => void;
}
type Action =
{ action: "moveTo"; args: [x: number, y: number]; } |
{ action: "lineTo"; args: [x: number, y: number]; } |
{ action: "closePath"; args: []; }
Action
类型是 discriminated union其中 action
属性充当判别式。这将为您提供所需的行为:
const ctx = document.createElement('canvas').getContext('2d');
if (!ctx) throw new Error(); // make sure it's not null
invokeActions(ctx, [
{ action: 'moveTo', args: [0, 0] },
{ action: 'lineTo', args: [100, 100] },
{ action: 'closePath', args: ['error'] }, // error!
// -------------------------> ~~~~~~~
// Type 'string' is not assignable to type 'number'
{ action: 'closePath', args: [1, 2] } // error!
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ <--
// Type '[number, number]' is not assignable to type '[]'.
]);
当然,您希望能够计算这些类型,而不是手动写出它们。如果我们从 action
属性到 args
类型的映射接口(interface)开始,这很容易:
interface DrawableArgs {
moveTo: [x: number, y: number];
lineTo: [x: number, y: number];
closePath: [];
}
然后我们可以根据它们定义Drawable
和Action
:
type Drawable =
{ [K in keyof DrawableArgs]: (...args: DrawableArgs[K]) => void }
type Action<K extends keyof DrawableArgs = keyof DrawableArgs> =
{ [P in K]: { action: P, args: DrawableArgs[P] } }[K]
请注意,Drawable
现在是 mapped type在 DrawableArgs
上,而 Action
是在 microsoft/TypeScript#47109 中创造的分布式对象类型 。如果您扩充 DrawableArgs
,则 Drawable
和 Action
将自动更新。
剩下的就是从实现者方面使这项工作正常进行。这有点棘手,因为如果您只是注释,您将在 forEach()
回调中收到错误:
const invokeActions = (
target: Drawable, actions: Action[]) => {
actions.forEach(({ action, args }) => {
target[action](...args); // error
// ~~~~~~~
// A spread argument must either have a tuple type or be passed to a rest parameter
});
}
发生这种情况是因为 target[action]
和 args
被解释为具有两个独立的联合类型。编译器已经失去了它们之间的相关性,因此无法确定您使用的参数实际上与操作相对应;它担心 action
可能是 "closePath"
,而 args
是 [number, number]
。这不可能发生,但编译器会失去跟踪并且无法确定。
缺乏对相关联合类型的直接支持是 microsoft/TypeScript#30581 的主题。 。 microsoft/TypeScript#47109(上面提到的)详细介绍了针对此类情况的推荐方法...即从联合重构到 generics 。这个想法是使用“基本”接口(interface),并用通用 indexes into 表示您的所有操作。该接口(interface)并转换为该接口(interface)上的映射类型。
事实上,DrawableArgs
类型充当基本接口(interface),Drawable
和 Action
是适当的映射类型和映射类型的通用索引。我们可以从 Drawable
开始,并从中派生出 Action
,但如果我们这样做,它将不符合 microsoft/TypeScript#47109,并且编译器也不会能够遵循它。
无论如何,唯一要做的就是确保 forEach()
回调实际上是 K extends keyof DrawableArgs
中的通用函数:
const invokeActions = (
target: Drawable, actions: Action[]) => {
actions.forEach(<K extends keyof DrawableArgs>({ action, args }: Action<K>) => {
target[action](...args); // okay
});
}
这是可行的,因为现在 action
是泛型类型 K
。因此,target[action]
的类型为 Drawable[K]
,其计算结果为 (...args: DrawableArgs[K]) => void
.并且 args
的类型为 DrawableArgs[K]
,因此允许调用。
关于typescript - 如何使用具有动态参数计数的方法输入动态对象,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/77607818/