简体   繁体   中英

Using Typescript Generics to restrict a functions input parameter

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?

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 } */ })

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM