Home > Mobile >  Constraining an interface/type to function values in TypeScript
Constraining an interface/type to function values in TypeScript

Time:11-25

I have implemented a class that handles 2-way communication between processes, and while trying to add type-safety to the API's I have run into an issue that I cannot seem to crack.

I am trying to essentially create a type-safe interface that allows me to use the same class in multiple process where I can define the connected API/exposed API, so that I can mitigate confusion issues:

// Simplified example

// API definitions
interface ExternalAPI {
  add(x: number, y: number): number;
}
interface InternalAPI {
  subtract(x: number, y: number): number;
}

// In use:
const bridge = new IPCBridge<ExposedAPI, ConnectedAPI>({
  subtract: (x, y) => x - y, // Works, compiler happy
});
bridge.invoke("add", 3, 3); // Works, compiler happy
bridge.invoke("nonexistent"); // Does not work, compiler mad

However, while my implementation works my types do not; and I cannot get the above generics, specifically, to compile without telling the compiler to ignore issues which I would prefer to not do.

I've implemented the above generics as so:

type ValidApiType = {
    [funcName: string]: (...args: unknown[]) => unknown;
};

class IPCBridge<InternalAPI extends ValidApiType, ExternalAPI extends ValidApiType> {
  // . . .
}

However, when trying to actually use this the compiler throws an error at the first generic:

interface ApiA {
  add(x: number, y: number): number;
}
/*
Type 'ApiA' does not satisfy the constraint 'ValidApiType'.
  Index signature for type 'string' is missing in type 'ApiA'.
*/

Similarly, if I were to try to have the interface extend ValidApiType, a similar error appears:

interface ApiA extends ValidApiType {
  add(x: number, y: number): number;
}
/*
Property 'add' of type '(x: number, y: number) => number' is not assignable to 'string' index type '(...args: unknown[]) => unknown'.
*/

I must be misunderstanding something. I have a selection of helper types to abstract the API function names from the interfaces keys and the args/return values of the values, and those all work just fine. I have even tried swapping the index signature to include symbol but still no luck. I just fail to understand how TS does not consider the keys to be a string. I've tried looking up the specific TS error codes but that did not really get me anywhere either, I just really don't understand how the types differ.

You can see exactly what is happening in this typescript playground.

CodePudding user response:

So I was trying to do something similar to this and got your playground working in this playground.

In ApiA & ApiB the interfaces are indexed by functions to type number, Not what is expected as a function name (string) indexing the function itself

Also when using this pattern all functions should have the same signature in order for typescript to work and not get mad at you because it will start saying missing property and so on.

I can provide some other examples for other cases if you need as I really do like this pattern of usage

Hope this helps.

CodePudding user response:

I was able to somewhat resolve this after looking at Ali's playground which gave me a better idea of what was going on.

By changing the ValidApiType to contain the signature (...args: any[]) => any and then updating the interfaces being used to define the API's to extend ValidApiType, I still get type-safety when defining the API's and the interfaces work perfectly with the generics as well. It's not ideal, as I would prefer not to have to extend the base type and nor would I like to use any, but it seems in this case using unknown or never just doesn't work.

// Base Implementation
type ValidApiType = {
    // Changed this from (...args: unknown[]) => unknown to use any.
    [funcName: string]: (...args: any[]) => any;
};

class IPCBridge<InternalAPI extends ValidApiType, ExternalAPI extends ValidApiType> {}

// Extending works, and enforces each value is a function.
interface ApiA extends ValidApiType {
    add(x: number, y: number): number;
    foo(): void;
    bar: [1, 2, 3]; // This would show an error
}

// Using the interface works.
IPCBridge<ApiA, {}>;

It's not fully ideal, but it gets me out of my current pickle and does what I want it to. If someone can come up with a solution/explanation on how to get it working with unknown instead of any and/or without having to extend ValidApiType I'm happy to mark it as the correct answer!

You can see the working version in this playground

  • Related