TypeScript:提取字符串文字作为函数参数

TypeScript: Extract string literal as function param

我正在使用 meteor.js 和 TypeScript 并尝试制作强类型的流星方法。

为此,我创建了一个文件,其中包含我的方法的类型定义,如下所示:

export interface ClientWithSecret {
    id: string;
    secret: string;
}

export interface MeteorMethod {
    name: string;
    args: any[];
    return: any;
}

export interface NewGameMethod extends MeteorMethod {
    name: "newGame";
    args: [auth: ClientWithSecret];
    return: string;
}

export interface NewClientMethod extends MeteorMethod {
    name: "newClient";
    args: [];
    return: ClientWithSecret;
}

export interface LoginMethod extends MeteorMethod {
    name: "login";
    args: [auth: ClientWithSecret];
    return: true | ClientWithSecret;
}

export type ValidMethods = NewGameMethod | NewClientMethod | LoginMethod;

现在我正在尝试创建一个方法,将普通的流星方法(使用回调...)包装到一个返回承诺的函数中,如下所示:

export function meteorCallAsync<T extends MeteorMethod>(methodName: T["name"], args: T["args"]): Promise<T["return"]> {
    return new Promise((resolve, reject) => {
        Meteor.call(methodName, ...args, (error: Meteor.Error, result: T["return"]) => {
            if (error) {
                reject(error);
            }
            resolve(result);
        });
    });
}

这似乎很有效。我可以等待这样的流星方法

const retVal = await meteorCallAsync<NewGameMethod>("newGame", [getClientWithSecret()]);

并且 TypeScript 实际上检查字符串 "newGame" 是否等于定义为 NewGameMethod 名称的字符串文字。完美,但我有两个问题,因为我是 TypeScript 的新手:

编辑: 我在下面添加了 newGame 方法的实现。问题是我不知道如何告诉 TypeScript Meteor.call(name, ...args, (error, result)=>{}) 实际上调用了 Meteor.methods

中定义的函数
Meteor.methods({
    // create a new game
    newGame(auth: ClientWithSecret) {
        if (!isValidClient(auth)) {
            console.error(`client invalid ${auth.id}`);
            return;
        }
        let randomId,
            newIdFound = false;
        while (!newIdFound) {
            randomId = Random.id();
            const game = GamesCollection.findOne({ _id: randomId });
            if (!game) {
                newIdFound = true;
            }
        }
        GamesCollection.insert({
            _id: randomId,
            hostId: auth.id,
            clientIds: [auth.id],
            players: [],
            createdAt: new Date(Date.now()),
        });
        return randomId;
    },
    newClient(): ClientWithSecret {
        //implementation
    },
    login(auth: ClientWithSecret): true | ClientWithSecret {
        // returns true if login successful, new ClientWithSecret if credentials invalid
    },
});

背景

您不必为每个函数键入接口,因为该信息已经存在于您的代码库中的某个地方。如果您知道它们是函数本身的类型,那么您可以使用 ReturnType and Parameters 从函数的类型中导出 args 和 return 的类型。我们在这里缺少的部分是函数名称和函数类型之间的关联。

我不熟悉 Meteor,所以我不得不查看文档以了解其工作原理。事实证明,这些类型的定义非常松散。

Meteor.call() 允许您传递带有任何参数的任何函数名称。

function call(name: string, ...args: any[]): any;

像您在这里做的那样围绕这个函数创建一个包装器是明智的。您将获得更好的类型安全和更好的自动完成支持。您可以使用声明合并来增加包类型,但包装它更容易实现。

可调用函数名称通过使用方法字典对象调用 Meteor.methods() 来定义。

function methods(methods: {[key: string]: (this: MethodThisType, ...args: any[]) => any}): void;

解决方案

我们想获取您的特定词典的类型。我们将使用一个中间变量而不是在 Meteor.methods() 中定义方法,这样我们就可以在该变量上使用 typeof 来获取您的字典类型。

// define the methods
const myMethods = {
  newGame(auth: ClientWithSecret) {
....
}

// set the methods on Meteor
Meteor.methods(myMethods);

// get the type
type MyMeteorMethods = typeof myMethods;

然后我们使用 MyMeteorMethods 类型来注释您的 meteorCallAsync 函数。

export function meteorCallAsync<T extends keyof MyMeteorMethods>(
  methodName: T, 
  ...args: Parameters<MyMeteorMethods[T]>
): Promise<ReturnType<MyMeteorMethods[T]>> {
  return new Promise((resolve, reject) => {
      Meteor.call(methodName, ...args, (error: Meteor.Error, result: ReturnType<MyMeteorMethods[T]>) => {
          if (error) {
              reject(error);
          }
          resolve(result);
      });
  });
}
  • T是方法名,必须是你字典里的关键字。
  • MyMeteorMethods[T] 是方法的类型。
  • 您的 args 与方法的参数匹配。我将 args 更改为 ...args 以便您可以单独传递参数而不是在数组中。
  • 您的 return 类型是方法 return 类型的 Promise

现在调用函数时不需要设置任何类型。 Typescript 可以根据 methodName 推断出正确的类型。您会在需要的地方出现错误,在不需要的地方不会出现错误。

const x = async () => {

  // ok to call with the correct arguments
  const retVal1 = await meteorCallAsync("newGame", getClientWithSecret());
  
  // error if required arguments are missing
  // 'Arguments for the rest parameter 'args' were not provided.'
  const retVal2 = await meteorCallAsync("newGame");

  // ok to call with no arguments if the method doesn't require any
  const retVal3 = await meteorCallAsync("newClient");

  // error if calling an invalid method name
  // 'Argument of type '"invalidFunc"' is not assignable to parameter of type '"newGame" | "newClient" | "login"''
  const retVal4 = await meteorCallAsync("invalidFunc");
}

高级

如果您想在方法对象的任何方法中使用 this,则需要一些技巧。我们想要在此处使用的一些类型(例如 MethodThisType)未导出,因此我们需要向后工作以获取它们。

type MeteorMethodDict = Parameters<typeof Meteor.methods>[0]

这为我们提供了该方法字典对象的类型,其中每个条目都是一个 this 类型为 MethodThisType 的函数。

我们希望确保您的方法扩展了 MeteorMethodDict 类型,而不会扩大类型并丢失有关您的特定方法的信息。所以我们可以通过一个强制类型的身份函数来创建方法。

const makeMethods = <T extends MeteorMethodDict>(methods: T): T => methods;

现在您可以在任何方法中使用 this,它将具有正确的类型。

const myMethods = makeMethods({
  newGame(auth: ClientWithSecret) {
    const userId = this.userId;
...

我们从 type MyMeteorMethods = typeof myMethods 得到的类型将包含 this 类型,无论您是否使用它。

type MyMeteorMethods = {
    newGame(this: Meteor.MethodThisType, auth: ClientWithSecret): any;
    newClient(this: Meteor.MethodThisType): ClientWithSecret;
    login(this: Meteor.MethodThisType, auth: ClientWithSecret): true | ClientWithSecret;
}