【问题标题】:TypeScript: Extract string literal as function paramTypeScript:提取字符串文字作为函数参数
【发布时间】:2021-03-19 20:09:54
【问题描述】:

我正在使用流星.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 的新手:

  • 是否可以完全省略 meteorCallAsync 的第一个参数并让 TypeScript 编译器填充它?我已经将类型定义为泛型,所以编译器确实有必要的信息来填写它,但我不知道 TypeScript 是否支持这一点

  • 有没有办法将MeteorMethod 接口定义为某种无法实例化的抽象接口? ValidMethods 实际上是否是更适合 meteorCallAsync&lt;T extends ValidMethods&gt; 的类型?有没有办法让我强制每个方法实际上必须有 nameargsreturn

编辑: 我在下面添加了newGame 方法的实现。问题是我不知道如何告诉 TypeScript Meteor.call(name, ...args, (error, result)=&gt;{}) 实际上调用了 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<T>Parameters<T> 从函数类型派生argsreturn。您只需要知道从方法名称到方法类型的关联。您可以创建一个接口,其中键是名称,值是方法,但这可能已经存在于 Meteor 类型中。我将查看 Meteor 包类型,以便给您一个可靠的答案。
  • 好的,Meteor.call 的定义非常松散且无用 function call(name: string, ...args: any[]): any; 似乎您使用 Meteor.methods() 定义了方法,因此我们可以利用它来获取应用程序方法的类型.
  • 我已经添加了我对 newGame() 流星方法的实现,并解释了为什么我试图用我自己的类型来定义它,希望这很有意义。如果有更高级的方法可以直接将Meteor.call 耦合到没有我的解决方法的方法,我当然会很高兴听到它们。
  • 您可以查看声明合并。我认为您正在做的事情很好,但是您可以用更少的 TS 代码来做到这一点。我正在为你解答:)

标签: typescript meteor typescript-typings string-literals


【解决方案1】:

背景

您不必为每个函数都输入接口,因为这些信息已经存在于代码库中的某个位置。如果您知道它们是函数本身的类型,那么您可以使用 ReturnTypeParameters 来派生 args 的类型以及函数类型的返回值。我们这里缺少的部分是函数名和函数类型之间的关联。

我不熟悉 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,以便您可以单独传递参数,而不是在数组中传递。
  • 您的返回类型是方法返回类型的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;
}

【讨论】:

  • 非常感谢您的详细解答!我明天会试试这个。我的一个心理障碍是“我不希望客户知道一个方法的整个实现”,但那是因为我没有内化任何特定于 TS 的东西实际上不会出现在源代码中。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-07-18
  • 2015-10-25
  • 1970-01-01
  • 2022-11-10
  • 2011-12-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多