kashahkashah
kashahkashah

Reputation: 777

Ensure every function in an interface passed as a generic is asynchronous

suppose I have an abstract class which takes a generic

export abstract class RegisterableClass<InstanceType>

and an implementer below as such:

class UserService extends RegisterableClass<IUserService> implements IUserService {
  someConstant: 42,
  async getAllUsers: (): User[] => {...}
}

with an interface like:

interface IUserService {
  someConstant: number;
  getAllUsers: () => Promise<User[]>;
}

I want to ensure that the passed generic, IUserService, in this particular case, are all asynchronous.

Can I do this statically with TypeScript somehow?

maybe using Extract<keyof IUserService, Function> ? and then passing that to something in turn?

That is, if you attempt to extend RegisterableClass and pass it a generic in which not all functions are async, typescript will not compile

Upvotes: 0

Views: 104

Answers (1)

jcalz
jcalz

Reputation: 329838

If I understand correctly you'd like to constrain the type parameter passed to RegisterableClass to be something whose methods are all asynchronous (meaning that they return promises).

If so, you can achieve that like this:

type AllMethodsAreAsync<I> = {
    [K in keyof I]: I[K] extends (...args: any) => infer R ?
    R extends Promise<any> ? I[K] : (...args: any) => Promise<R> :
    I[K]
}

export abstract class RegisterableClass<I extends AllMethodsAreAsync<I>> {
    // ... something involving I, hopefully
}

The type function AllMethodsAreAsync<I> will be equivalent to I if I is an object type whose function-valued properties return promises. But if I has any function-valued properties that don't return promises, the corresponding property of AllMethodsAreAsync<I> will be changed to return a promise instead.

Then, if I extends AllMethodsAreAsync<I> passes the generic constraint, great. Otherwise, you should get an error telling you exactly what from I doesn't work. Like this:

// adding this so standalone example works
type User = { u: string }; 

interface IOkayService {
    someConstant: number;
    getAllUsers(): Promise<User[]>;
}

type OkayService = RegisterableClass<IOkayService>; // okay

interface IBadService {
    someConstant: number;
    getAllUsers(): Promise<User[]>;
    getSomethingSynchronously(x: string): number;
}

type BadService = RegisterableClass<IBadService>; // error!
//                                  ~~~~~~~~~~~
// Type 'IBadService' does not satisfy the constraint 'AllMethodsAreAsync<IBadService>'.
// Types of property 'getSomethingSynchronously' are incompatible.
// Type '(x: string) => number' is not assignable to type '(...args: any) => Promise<number>'.
// Type 'number' is not assignable to type 'Promise<number>'.

Playground link

Does that do what you want? Hope that helps; good luck!

Upvotes: 3

Related Questions