1

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?

JHH
  • 8,567
  • 8
  • 47
  • 91
  • Just use `type` keyword instead of `interface` in `MyApi`. See [this](https://tsplay.dev/mZQeeN) and [this](https://stackoverflow.com/questions/37233735/interfaces-vs-types-in-typescript#answer-64971386) answer – captain-yossarian from Ukraine Jan 19 '23 at 10:16
  • Wow. I thought I knew most of TS by now, but this caught me off guard. At first I didn't understand _why_ that works, but this sentence made the penny drop: "In contrast, interfaces are not final the moment you declare them. There is always the possibility of adding new members to the same interface due to declaration merging." I pretty much always use interfaces over types where possible, it seems like I should revisit that principle ... – JHH Jan 20 '23 at 08:00
  • Do you want to put this into an answer so I can accept it? – JHH Jan 20 '23 at 08:01

1 Answers1

2

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