1

Problem

I have created a Typescript package that shares types between my backend node firebase cloud functions and my fronted React client that calls them. Below are some samples of the types.

interface FirstFunctionInput {
  x: number
}

interface SecondFunctionInput {
  y: string
}

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

This is how I am calling the functions currently, using the firebase sdk:

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

My problem here is

  1. There is no guarantee that the function 'firstFunction' exists
  2. Even if it does, how can I be sure that it accepts a parameter such as { x: 21 }.

So what I am trying to achieve is a way to write a utility method using Generics that overcomes these problems.

Ideal solution

Below is an example of how I'd (ideally) like the API to look. I have provided an example where the wrong input type is provided to the function, and I expect Typescript to show an error.

callFirebaseFunction<FirstFunction>({ y: 'foo' })
                                    ^----------^
                    show error here, since FirstFunction must take a FirstFunctionInput

Current (but not quite there yet) solution

I have come close to achieving this, with an API that works, however it requires the input params to be specified at the calling location:

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

This is not ideal because the input type needs to be known by the programmer, in order to specify it.

Final Words

I have tried all different combinations of interfaces that are extended, abstract classes that are extended and even the builder pattern, but I cannot find a way to tie this all together. Is something like this even possible?

1
  • Does this meet your needs? If so, I'll write up an answer. If not, please elaborate on exactly what is missing, preferably with a minimal reproducible example version suitable for dropping as-is into a standalone IDE where the issue is demonstrated. Commented Apr 10, 2021 at 20:30

1 Answer 1

4

Since there is no generic used in httpsCallable, we should wrap it with function and use generic to restrict the type.

Some knowledge about Currying will be used here, in this way, its structure is the same as 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 want to specify the type of output, that is to say

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

Due to the type of HttpsCallableResult is such a interface in source code:

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

Then the type of output extending {data: any} would be better.

Finally, we can achieve that like following:

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 } */ })
Sign up to request clarification or add additional context in comments.

6 Comments

This is awesome and is working great! I actually have a follow up question and wondering if you can help? I would like to extend this functionality so that I can specify both the input, as well as the expected output. Would you have any idea how to achieve this?
To restrict type of result of Promise? like this one? callFirebaseFunction('firstFunction')({ x: 1 }).then((res) => /* restrict type of res */)
yes exactly like that. I thought once I get the first part solved I'll extend the solution to include the return types, but I have never seen currying before and I am struggling to do this as well..!
sure thing, wait a sec, I am updating my answer
If extends it, callFirebaseFunction('test')({ x: 1 }) will not show any error possibly, so you can try generics to define this type BaseFunction<Input, Output> = { input: Input; output: { data: Output } }, and then define firstFunction... with it.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.