Using TypeScript to extract a string literal as a parameter in a function

Currently experimenting with meteor.js and TypeScript to create strongly typed meteor methods. I have defined type definitions for my methods in a file as follows:

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;

Now, I am working on wrapping normal meteor methods with callbacks into functions that return promises like so:

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);
        });
    });
}

This implementation works perfectly fine. I can await a meteor method call like this:

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

I do have a couple of questions since I'm new to TypeScript:

  • Is it possible to exclude the first parameter in meteorCallAsync and let TypeScript automatically fill it in based on the defined generic type?

  • Can the MeteorMethod interface be defined as an abstract interface that cannot be instantiated? Also, would it be more appropriate to use ValidMethods as the type for

    meteorCallAsync<T extends ValidMethods>
    and how can I enforce each method to have a name, args, and return?

EDIT: Below is my implementation of the newGame method. The challenge lies in informing TypeScript that

Meteor.call(name, ...args, (error, result)=>{})
actually triggers the function defined in 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
    },
});

Answer №1

The Context

No need to constantly define interfaces for each function when the required information is already present within your codebase. If you're aware of the function's type, you can utilize ReturnType and Parameters to infer the types for arguments and return values based on the function's type. The missing link here is binding the function names with their respective types.

I'm not well-versed in Meteor, so I had to reference the documentation to understand its functionality. It seems that the types are quite loosely defined.

Meteor.call() allows any function name to be passed with any set of arguments.

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

Implementing a wrapper around this function, as you're doing, is intelligent. It not only enhances type safety but also improves autocomplete support. Although declaration merging could be utilized to bolster package types, wrapping proves to be the simpler choice for implementation.

The function names callable are established by invoking Meteor.methods() with a dictionary object containing the methods.

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

The Resolution

We aim to deduce the type of your specific dictionary. By utilizing an intermediary variable instead of defining the methods within Meteor.methods(), we gain the ability to use typeof on that variable to ascertain your dictionary's type.

// method definitions
const myMethods = {
  newGame(auth: ClientWithSecret) {
....
}

// assigning methods to Meteor
Meteor.methods(myMethods);

// obtaining the type
type MyMeteorMethods = typeof myMethods;

This MyMeteorMethods type is then applied to annotate your meteorCallAsync function.

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 represents the method name, which must be a key within your dictionary.
  • MyMeteorMethods[T] denotes the type for the method.
  • Your args align with the parameters of the method. Changing args to ...args permits passing arguments individually rather than in an array.
  • The return type comprises a Promise of the method's return type.

When calling the function, no explicit typing is necessary. TypeScript infers the correct types based on the methodName. Errors occur where warranted while allowing smooth execution elsewhere.

const x = async () => {

  // successful call with proper arguments
  const retVal1 = await meteorCallAsync("newGame", getClientWithSecret());
  
  // error on missing required arguments
  // 'Arguments for the rest parameter 'args' were not provided.'
  const retVal2 = await meteorCallAsync("newGame");

  // valid call with no arguments if method doesn't demand any
  const retVal3 = await meteorCallAsync("newClient");

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

Pinnacle

To leverage this within any methods in your methods object necessitates some finesse. As certain crucial types (like MethodThisType) aren't openly exported, a workaround is imperative to acquire them.

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

This furnishes us with the type for the method dictionary, where each entry pertains to a function having a this type of MethodThisType.

We desire your methods to extend the MeteorMethodDict type without diluting it and forfeiting information about your particular methods. Hence, employing an identity function enforces the type.

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

Now, utilizing this inside any method ensures the correct type association.

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

The

type MyMeteorMethods = typeof myMethods
emerges including the this type, regardless of its usage.

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

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Encountering an error while unit testing Angular components with MatDialog: "Error: <spyOn>: open has already been spied upon."

Once I create an HTML file with a button, I trigger a poll to appear after an onClick event. Then, when the "submit" button is clicked on the dialog window, it closes and I intend to execute subsequent methods. In my TypeScript file: openDialogWindow() { ...

The submit button seems to be unresponsive or unreactive

As a newcomer to Angular 2 and Typescript, I am in the process of building a web application. I have created several input fields and, following user submission via a button, I want to log the inputs to the console. However, it seems like my button is not ...

Tips for retrieving the most recent number dynamically in a separate component without needing to refresh the page

Utilizing both the Helloworld and New components, we aim to store a value in localStorage using the former and display it using the latter. Despite attempts to retrieve this data via computed properties, the need for manual refreshing persists. To explore ...

The subsequent code still running even with the implementation of async/await

I'm currently facing an issue with a function that needs to resolve a promise before moving on to the next lines of code. Here is what I expect: START promise resolved line1 line2 line3 etc ... However, the problem I'm encountering is that all t ...

Change the parent title attribute back to its original state

This is in contrast to queries similar to the one referenced here. In my scenario, there is a child element within a parent element (specifically a matSelect within a matCard, although that detail may not be significant) where the parent element has a set ...

Tips on preventing image previews from consuming too much text data while updating a database in Angular 12 using Material UI for image uploads within a FormGroup object

Currently working with Angular 12 and Angular Material for image uploads with preview. I have a formgroup object below, but I'm running into issues with the 197kb image preview text being inserted into the database. Despite trying setValue/patchValue/ ...

Is it possible to pass multiple parameters in Angular by utilizing the click() function?

Is there a method for passing parameters with click() in Angular? <a asp-action="CreateSales" (click)="CreateSales(productname='pa', price='16.5')">Some Text</a> I am still learning Angular and would appreciat ...

Mistakes encountered following the installation of lodash in Angular 2

After adding lodash to my Angular 2 project, I encountered a significant number of errors. To troubleshoot, I created a new project using the CLI: ng new tester, then I added lodash with npm install --save @types/lodash. When I ran ng serve, I received the ...

Creating a button that displays the current day with Angular

I'm in the process of developing a timetable app that features buttons for the previous day, current day, and next day. How can I implement a button to specifically show the current day? HTML File <button type="button" (click)="previousDay()" ...

Issue encountered while developing custom Vuejs + Typescript plugin

In my index.ts and service plugin files, I have this structure: https://i.sstatic.net/Oh3Gq.png service.ts declare interface Params { title: string; description?: string; type?: string; duration?: number; } export default class ServiceToast { ...

What are the most effective techniques for utilizing promise.all in your codebase?

When trying to consolidate responses from two API calls in the code below, I'm facing an issue where Promise.all is not being invoked. Any suggestions on what might be implemented incorrectly and the best practice to achieve this using Promise.all? T ...

Obtain the Enum's Name in TypeScript as a String

I am currently looking for a solution to transform the name of an enum into a string format. Suppose I have the following Response enum, how can I obtain or convert 'Response' into a string? One of my functions accepts any enum as input and requi ...

challenging situation with IONIC 2

Building an app using Ionic 2 and trying to incorporate the ble-plugin. Following the installation steps: $ cordova plugin add cordova-plugin-ble-central In my page's TS, I included the following code: import {Page, Alert, NavController} from &apos ...

Looking for a solution to resolve the issue "ERROR TypeError: Cannot set property 'id' of undefined"?

Whenever I attempt to call getHistoryData() from the HTML, an error message "ERROR TypeError: Cannot set property 'id' of undefined" appears. export class Data { id : string ; fromTime : any ; toTime : any ; deviceType : string ...

Destructuring arrays of objects in ES6 with conditions

Check out the demo on stackblitz: here The scenario is this: the server responds in a specific format, and based on certain conditions, we need to determine whether to show or hide buttons on a page. Each button has its own click function, which is why th ...

How many times does the CatchError function in Angular 6 response interceptor get executed?

While working on my Angular project, I implemented an interceptor to intercept all requests and responses. However, I noticed that the function responsible for validating errors in the responses is being executed 7 times. Upon further investigation, I dis ...

Troubleshooting a 400 Bad Request Error when calling ValidateClientAuthentication in ASP.NET WebApi, even after using context.Validated()

I am currently facing an issue with my angularjs HTML client connected to a WebApi project. The APIs work fine when tested using POSTMAN or other REST clients. However, when I try to use browsers with my angularjs client, the browsers always initiate prefl ...

How do I specify the return type of a function in Typescript that can return more than one type?

I am facing a situation where I have a method called getFilters that retrieves various types of values. getFilters(): IListFilteringType {...} type IListFilteringTypeMultiSelect = (string | number)[]; type IListFilteringType = boolean | string | number | ...

Convert all existing objects to strings

I have a type that consists of properties with different data types type ExampleType = { one: string two: boolean three: 'A' | 'Union' } Is there an easier way to define the same type but with all properties as strings? type Exam ...

There was a typo in the Next.js TypeScript custom Document error message: TypeError: The Document class constructor cannot be invoked without using the 'new' keyword

I encountered a problem with my website that has a customized Document by _document.js. When I tried running yarn dev, I received the error: TypeError: Class constructor Document cannot be invoked without 'new' I spent a considerable amount of t ...