Search code examples
javascripttypescriptkeyof

How to use TypeScript "keyof" to create interface to generic API interface?


I am having trouble using TypeScript's type system to model a generic interface to call from Lua into TypeScript. For this to work, the developer must define a TypeScript interface defining the TypeScript methods that Lua can call into. The details don't matter much here, as the simple example below illustrates my confusion.

KeyofDemo

As you can see from the code above, I have "ApiInterface", which defines arbitrary methods that a developer may want to invoke from Lua. All such methods take a first argument that is an object (akin to 'this'), and the remaining arguments can be whatever the developer needs to pass from Lua into the TypeScript method. In practice, the only types supported for these remaining arguments are primitives like number, string, boolean, null. In the example code above, I defined 3 such API methods, but the developer may define whatever methods they want. The whole point of this Lua-to-TypeScript module is to be generic, and not be tied to any particular definition of "ApiInterface".

The runtime eventually packages up the Lua-to-TypeScript method call into an object of type ApiCall, which you can see is defined with fields for the method name, the JavaScript object to use for "this", and the remaining arbitrary arguments.

To illustrate my confusion, I have shown a KeyofDemo to illustrate the TypeScript error I am getting. There is a "run" method in the KeyofDemo class that wants to take the ApiCall object and invoke the corresponding TypeScript/JavaScript function.

You can see the red-squiggly under the actual invocation of the method in the two cases. One case is where there are no additional arguments (beyond the required "this" argument), and the other case for when ApiCall.args is actually populated with an array of arguments to pass from Lua to TypeScript.

The error message from the first red-squiggly case is: A spread argument must either have a tuple type or be passed to a rest parameter.

The error message from the second red-squiggly case is: Expected 3 arguments but got 1.

Neither of these error messages makes any sense to me. If I try to compile the above TypeScript code, the compiler reports similar errors.

How can I get TypeScript's type system to best model the behavior I have described? For now, I have worked around the problem by changing lines 23 and 25 to case "this.api" to "any", and this shuts up the TypeScript compiler. But I would like to know how to properly model this problem.


Solution

  • The first error is because Javascript/Typescript does not support using spread operators in parameters, unless your function is configured to do so.

    You have to configure the function to accept variadic parameters.

    interface ApiInterface {
        apiMethod0(thisObj: Object, ...args: any[]): void
        apiMethod1(thisObj: Object, ...args: [string]): void
        apiMethod2(thisObj: Object, ...args: [number, boolean]): void
    }
    

    See this code in playground

    The second error is because the methods need to be all of the same "shape", or have the same # of parameters. In this case we kill both birds with the same stone.