Leveraging Typescript Generics for limiting the input parameter of a function

Issue

I have developed a Typescript package to share types between my backend node firebase cloud functions and frontend React client that accesses them. Provided below are examples of the types:

interface FirstFunctionInput {
  x: number
}

interface SecondFunctionInput {
  y: string
}

// lists all available functions
enum FirebaseFunction {
  FirstFunction = "firstFunction",
  SecondFunction = "secondFunction"
}

Currently, I am invoking the functions using the firebase sdk:

firebase.funtions.httpsCallable('firstFunction')({ x: 21 })
  .then(result => { ... })

The challenge at hand is:

  1. There is no assurance that the function 'firstFunction' exists.
  2. Even if it does, how can one confirm that it accepts a parameter like { x: 21 }.

Thus, I aim to create a utility method using Generics that resolves these issues.

Preferred Solution

Desired API representation is provided in the example below. A scenario where an incorrect input type is passed to the function is outlined, anticipating Typescript to flag an error.

callFirebaseFunction<FirstFunction>({ y: 'foo' })
                                    ^----------^
                    display error here as FirstFunction should accept a FirstFunctionInput

Current Progress (not fully achieved yet)

I have made progress towards this goal with a functioning API, although input parameters must be specified during the call:

callFirebaseFunction<FirstFunctionInput>(
  FirebaseFunction.FirstFunction,
  { x: 21 }
)
.then(result => { ... })

This setup is not ideal since the programmer needs knowledge of the input type to specify.

Closing Thoughts

Despite attempting various strategies involving interfaces extension, abstract classes extension, and even the builder pattern, I struggle to consolidate everything. Is such a solution feasible?

Answer №1

When using the httpsCallable method without specifying a generic type, it is advisable to wrap it in a function and use generics to define the type.

Understanding concepts like Currying can be helpful in maintaining the structure of the code similar to the original.

interface FirstFunctionInput {
  x: number
}

interface SecondFunctionInput {
  y: string
}

interface FunctionInputs {
  firstFunction: FirstFunctionInput,
  secondFunction: SecondFunctionInput
}

function callFirebaseFunction<K extends keyof FunctionInputs>(fn: K) {
  return function (input: FunctionInputs[K]) {
    return firebase.functions().httpsCallable(fn)(input);
  };
};

callFirebaseFunction('firstFunction')({ x: 1 }) // OK
callFirebaseFunction('secondFunction')({ y: '1' }) // OK
callFirebaseFunction('test')({ x: 1 }) // ERROR
callFirebaseFunction('secondFunction')({ x: true }) // ERROR

Additional Update

If you wish to specify the output type for the response, you can do so by:

allFirebaseFunction('firstFunction')({ x: 1 }).then((res) => /* specify type of res */)

In the source code, the type of HttpsCallableResult interface is defined as follows:

export interface HttpsCallable {
    (data?: any): Promise<{data: any}>;
  }

It would be advantageous to extend the output type with {data: any}.

To achieve this, we can modify the code as shown below:

interface FunctionInputs {
  firstFunction: {
    input: FirstFunctionInput,
    output: { data: number }
  },
  secondFunction: {
    input: SecondFunctionInput,
    output: { data: boolean }
  }
}

function callFirebaseFunction<K extends keyof FunctionInputs>(fn: K) {
  return function (input: FunctionInputs[K]['input']): Promise<FunctionInputs[K]['output']> {
    return firebase.functions().httpsCallable(fn)(input);
  };
};

callFirebaseFunction('firstFunction')({ x: 1 }).then(res => { /* { data: number } */ })
callFirebaseFunction('secondFunction')({ y: '1' }).then(res => {/* { data: boolean } */ })

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

The argument with type 'void' cannot be assigned to a parameter with type 'Action'

Just getting started with Typescript and using VSCode. Encountering the following Error: *[ts] Argument of type 'void' is not assignable to parameter of type 'Action'. (parameter) action: void Here is the code snippet causing the err ...

Tips for displaying an element for each item chosen in a multi-select box

I currently have a situation where I am rendering for every selected element within my multiselect angular material selectbox. The rendering process functions correctly when I select an element, but the issue arises when I try to deselect one - it just ke ...

Issue: The module '@nx/nx-linux-x64-gnu' is not found and cannot be located

I'm encountering issues when trying to run the build of my Angular project with NX in GitHub Actions CI. The process fails and displays errors like: npm ERR! code 1 npm ERR! path /runner/_work/myapp/node_modules/nx npm ERR! command failed npm ERR! c ...

React typescript: When defining an interface in RouterProps, it is important to remember that it can only extend an object type or a combination of object types

For my React project, I decided to implement Typescript. After seeking assistance from Chatgpt, I was able to obtain this code snippet: import React from "react"; import { Route, Navigate, RouteProps } from "react-router-dom"; import { ...

Error Encountered: Monorepo Shared Package Not Detected in Docker-Compose Execution

In my development setup, I have organized a monorepo using lerna and yarn workspaces. All code is written in typescript and then compiled into javascript. However, I encountered an issue with sharing packages when running the monorepo with docker-compose. ...

Error TS2339: The 'phoneType' property cannot be found on the 'Object' data type

Below is the declaration of an object: export class Card { private _phones:Object[] get phones(): Object[]{ if(this._phones === undefined) this._phones = [] return this._phones } set phones(val:Object[]){ ...

Problem encountered in a simple Jest unit test - Unexpected identifier: _Object$defineProperty from babel-runtime

Struggling with a basic initial test in enzyme and Jest during unit testing. The "renders without crashing" test is failing, as depicted here: https://i.stack.imgur.com/5LvSG.png Tried various solutions like: "exclude": "/node_modules/" in tsconfig "t ...

Is it possible to deactivate the error message related to "Unable to ascertain the module for component..."?

I recently incorporated a new component into my TypeScript2+Angular2+Ionic2 project. Right now, I have chosen not to reference it anywhere in the project until it is fully developed. However, there seems to be an error thrown by Angular/ngc stating "Cannot ...

Disabling `no-dupe-keys` in ESLint does not seem to be effective

Currently, I am working on a project where I have incorporated Typescript and ESLint. However, I have encountered an issue with the error message stating: An object literal cannot have multiple properties with the same name. I am looking to disable this s ...

Tips for ensuring your controls function properly and seamlessly when switching to another page

I utilized the instructions from this post to implement a slider. However, I encountered an issue with the controller when navigating to subsequent pages. While the controller functions correctly on the initial page, it duplicates the same values on the fo ...

Search through an array of objects and assign a new value

I am facing a challenge with an array of objects structured as shown below: [ { "e_id": "1", "total": 0 }, { "e_id": "3", "total": 0 } ] My objecti ...

Choose a specific location on a vehicle illustration within a pop-up window. The image should be partitioned into 6 separate sections

I have a project for my client where they need to choose a car and then indicate where the damage is located on an image, which is divided into 6 sections. I'm struggling to figure out how to achieve this. I initially thought z-index would work, but ...

Error occurred when sending form data while uploading a file

Upon trying to upload a file using the formData.append(key, value);, an error message is displayed in the value section: The argument of type 'unknown' cannot be assigned to a parameter of type 'string | Blob'. Type '{}' is ...

Sending various kinds of generic types to React component properties

Currently, my team and I are working on a TypeScript/React project that involves creating a versatile 'Wizard' component for multi-step processes. This Wizard acts as a container, receiving an array of 'panel' component properties and p ...

When iterating through a table, an error occurs stating that the property "rows" is not available on type HTMLElement (

Issue Error TS2339 - Property 'rows' does not exist on type HTMLElement when looping through table in Angular 7 Encountering error when trying to loop through HTML table in Angular 7 Currently working with Angular 7 and facing an error while ...

Encountering issues while attempting to transmit several files to backend in React/NestJS resulting in a BAD REQUEST error

My goal is to allow users to upload both their CV and image at the same time as a feature. However, every time I attempt to send both files simultaneously to the backend, I encounter a Bad Request error 400. I have made various attempts to troubleshoot th ...

A guide to strictly defining the subclass type of object values in TypeScript

How do I enforce strict subclass typing for object values in the SHAPE_PARAMS definition? When using type assertion like <CircleParameter>, missing properties are not caught by linting. Is there a way to define subclass types strictly? const Shapes ...

Looking for a more efficient approach to writing React styles for color?

Desire I am interested in customizing colors within Material UI components. Moreover, I aim to develop a React Component that allows for dynamic color switching through Props. Challenge The current approach using withStyles results in redundant and lengt ...

Is there a way for me to verify that the key of one object is a subset of the keys of another object?

export const masterKeysObject = { MAIN: 'main', REDIRECT: 'redirect', DASHBOARD: 'dashboard', USER_ID_PARAM: ':userId', CREATE_NEW: 'create_new' } as const; type MasterKeys = keyof type ...

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 ...