typescript - 如何使用具有动态参数计数的方法输入动态对象

标签 typescript typescript-typings typescript-generics

我有一个方法:

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

然后我们可以根据它们定义DrawableAction:

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 typeDrawableArgs 上,而 Action 是在 microsoft/TypeScript#47109 中创造的分布式对象类型 。如果您扩充 DrawableArgs,则 DrawableAction 将自动更新。


剩下的就是从实现者方面使这项工作正常进行。这有点棘手,因为如果您只是注释,您将在 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),DrawableAction 是适当的映射类型和映射类型的通用索引。我们可以从 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],因此允许调用。

Playground link to code

关于typescript - 如何使用具有动态参数计数的方法输入动态对象,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/77607818/

相关文章:

typescript - 为任意对象属性声明 Typescript 类型

reactjs - 如何使用 Typescript、React 和 Webpack 导入 CSS 模块

typescript - 通用联合类型执行交集类型的棘手输入问题

javascript - Axios 泛型 Post 在 TypeScript 中返回错误类型

javascript - 如何在 typescript 中声明成功/失败返回类型

typescript - 为什么导入节点模块会破坏我在 atom-typescript 中的内部 Typescript namespace ?

Typescript 模板文字类型循环约束

javascript - 为什么我不能在 typescript 中向 String.prototype 添加新方法

oop - 如何在 Typescript 中将值从一个类的方法传递到另一个类的方法?

typescript - 如何使用 Typescript 和 Node 生成 REST API 文档?