micah
micah

Reputation: 8096

How to Create a Compile Error on Method Decorators in Typescript?

I am working on a library called expresskit that lets you use decorators to define routes/params/etc for express. I'm in the middle of refactoring and am thinking I need to limit the types of responses a route can have. As an example, here is how routes are created now-

export default class UserRouter {
  @Route('GET', '/user/:userId')
  public static getUser(@Param('userId') userId: number): any {
    return new User();
  }
}

A Route is applied to a static method. The static method may return a value directly or a Promise. I'd like to require promises going forward like this-

export default class UserRouter {
  @Route('GET', '/user/:userId')
  public static async getUser(@Param('userId') userId: number): Promise<User> {
    return Promise.resolve(new User());
  }
}

The reason being, the logic behind these routes are getting bloated and tangle to handle different types of responses. Since most routes will likely be asynchronous, I'd rather have cleaner core code by relying on async. My Route decorator function looks like this-

export default function Route(routeMethod: RouteMethod,
                              path: string) {                       
  return function(object: any, method: string) {
    let config: IRouteConfig = {
      name: 'Route',
      routeMethod: routeMethod,
      path: path
    };

    DecoratorManager.registerMethodDecorator(object, method, config);
  } 
}

I've created a generic manager service to keep track of where decorators are registered. In the sample I can get the class as well as the method name. I can reference this later like this- object[method].

On my decorator, I would like to require that the class method is asynchronous. But since I only get the object and method name I don't know if I can do that. How can I require the class method returns Promise<any>?

Upvotes: 3

Views: 1039

Answers (1)

Mattias Buelens
Mattias Buelens

Reputation: 20179

You need to add some types to indicate that your decorator factory returns a decorator function that only accepts property descriptors with the expected function signature (...any[]) => Promise<any>. I went ahead and made a generic type alias RouteFunction for it:

type RouteMethod = 'GET' | 'POST'; // or whatever your library supports

// The function types the decorator accepts
// Note: if needed, you can restrict the argument types as well!
type RouteFunction<T> = (...args: any[]) => Promise<T>;

// The decorator type that the factory produces
type RouteDecorator<T> = (
    object: Object,
    method: string,
    desc: TypedPropertyDescriptor<RouteFunction<T>> // <<< Magic!
) => TypedPropertyDescriptor<RouteFunction<T>>

// Decorator factory implementation
function Route<T>(routeMethod: RouteMethod, path: string) : RouteDecorator<T> {                       
  return (object, method, desc) => {
    // Actual route registration goes here
    return desc;
  } 
}

Example usage to demonstrate type checking:

class RouteExample {

    @Route('GET', 'test1') // works, return type is a Promise
    test1(): Promise<number> {
        return Promise.resolve(1);
    }

    @Route('GET', 'test2') // error, return type not a Promise
    test2(): number {
        return 2;
    }

    @Route('GET', 'test3') // error, property is a number rather than a function
    get test3(): Promise<number> {
        return Promise.resolve(3);
    }

}

Try it on the playground!

Upvotes: 5

Related Questions