JHH
JHH

Reputation: 9295

Define a super type for a dictionary of methods

Let's say I have a number of APIs which are all dictionaries of async methods, e.g.:

interface MyApi {
  foo(a: string): Promise<number>;
  bar(a: number, b: boolean): Promise<string>;
  baz(): Promise<void>;
}

interface MyAp2 {
  qux(a: string): Promise<void>;
}

And I want to define a type which any such interface (=a dictionary where each property is an async function) implements, but in a way so that interfaces containing properties that are NOT async functions (e.g. {foo: number;} or {foo(): number}) would not match. What could such a type look like?

I tried this:

type Api = {
  [name: string]: (...args: any[]) => Promise<any>;
}; 

But I cannot do

class Something<T extends Api> {
}

new Something<MyApi>();

Due to

TS2344: Type 'MyApi' does not satisfy the constraint 'Api'.   
  Index signature for type 'string' is missing in type 'MyApi'.

So the problem seems to be that a concrete set of functions does not have a general string index signature.

I managed to get something working like this, but it feels bulky:

type Api<T> = {
  [P in keyof T]: (...args: any[]) => Promise<any>;
};

class Something<T extends Api<T>> {
}

Now, new Something<MyApi>() works, but trying to do new Something<{foo: number;}>() fails, as intended: 🥳

TS2344: Type '{ foo: number; }' does not satisfy the constraint 'Api<"foo">'.   
  Types of property 'foo' are incompatible.     
    Type 'number' is not assignable to type '(...args: any[]) => Promise<any>'.

Is there a cleaner way of defining a type describing a set of async functions that doesn't have to use the "recursive" syntax T extends Api<T>, i.e., a simple non-generic type which any interface consisting only of async functions will fulfill?

Upvotes: 1

Views: 42

Answers (1)

You need to use type instead of interface for MyApi. Please see related answer and official explanation

Just to fill people in, this behavior is currently by design. Because interfaces can be augmented by additional declarations but type aliases can't, it's "safer" (heavy quotes on that one) to infer an implicit index signature for type aliases than for interfaces. But we'll consider doing it for interfaces as well if that seems to make sense

type MyApi = {                              // CHANGE IS HERE
    foo(a: string): Promise<number>;
    bar(a: number, b: boolean): Promise<string>;
    baz(): Promise<void>;
}

interface MyAp2 {
    qux(a: string): Promise<void>;
}

type Api = {
    [name: string]: (...args: any[]) => Promise<any>;
};

class Something<T extends Api> {
}

new Something<MyApi>(); // ok

Playground

In other words: interfaces are not indexed by the default whereas type are

You can also check my article

Please consider using interfaces over types. interfaces are safer. If you declare two interfaces with same name in different files but these files contain import/export there will be no declaration merging because it would be two different modules

Upvotes: 2

Related Questions