如何确保类的每个方法都必须具有固定的参数集

时间:2019-06-08 15:15:01

标签: typescript

让我们说我有一个类Controller,它具有一些方法。我希望我的controller类方法具有某些类型的3个参数。如何确保类中的每个方法都以定义的方式实现方法

我尝试使用该界面,但没有成功

   /**
   * I want every method of the UserController class to
   * have req, res and next as parameters. Parameters
   * other than req, res and next should not be allowed
   */

   class UserController {
      // correct implementation, as this method has
      // req, res and next as a parameter
      public async login(req: Request, res: Response, next: NextFunction) {
         // do some login stuff here
      }

      // wrong implementation, as req, res and next are missing
      public async signup(username, password) {
         // do some signup stuff here
      }
   }

我知道我可以为UserController创建一个接口,该接口定义具有固定参数集的UserController的每个方法。但是我想要一个更通用的解决方案,该解决方案应迫使开发人员遵循固定的模式。如果使用接口,则必须在每个代码检查过程中检查Controller的正确实现。

2 个答案:

答案 0 :(得分:2)

因此,您想要一个UserController类,其中每个方法(即每个函数值属性)都与某个调用签名完全匹配,如下所示:

type AcceptableMethod = (req: Request, res: Response, next: NextFunction) => Promise<void>;
type NextFunction = { noIdeaWhatYouWantHere(): void };

(我创建了一个虚拟NextFunction类型,以避免在独立IDE中出错)。

好吧,尽管它不如我想要的漂亮,但您可以做到。正如我在评论中提到的,a function which takes fewer parameters is assignable to a function which takes more parameters;因此看起来像async foo(){}的方法将与AcceptableMethod匹配,因为编译器不想要求foo()接受任何参数...因为这与接受那些参数相同,并且无视他们。无论您是否同意(有时您应该同意),这都会使操作更加麻烦,因为我们需要手动检查参数的数量。

无论如何,我们可以通过约束UserController来实现MeetsConstraints<UserController>来实现,其中MeetsConstraints<T>T兼容,并且前提是它满足您的约束。让我们用语言来描述约束。对于K中的每个属性键T(以及对应的值T[K]):

  • 如果T[K]是函数类型,则:
    • 它的参数数量应与AcceptableMethod完全相同,
    • 并且它应该与AcceptableMethod
    • 兼容
  • 否则,如果它不是函数类型,则T[K]可以是任何东西。

这是将其转换为代码的一种方法:

type MeetsConstraints<T> = { [K in keyof T]:
  T[K] extends (...args: any) => any ? (
    ParamsLength<T[K]> extends ParamsLength<AcceptableMethod> ? AcceptableMethod :
    T[K] extends AcceptableMethod ? Invalid<
      ["Expected", ParamsLength<AcceptableMethod>, "parameters, got", ParamsLength<T[K]>]
    > : AcceptableMethod
  ) : T[K]
};

我们需要定义ParamsLength<>

type ParamsLength<F extends (...args: any)=>any> = Parameters<F>['length'];

让我们谈谈Invalid<>。如果T[K]AcceptableMethod匹配,但没有正确数量的参数,则我们需要返回一个错误条件,以阻止K属性匹配。最简单的方法是返回类型never,但是错误消息是完全无法理解的:()=>Promise<void> is not assignable to never。相反,我们想使用“无效”类型,该类型会导致错误,但允许我们打印自定义消息。 Unfortunately this doesn't exist in TypeScript as of TS3.5,因此我们需要一种解决方法。在这里:

type Invalid<T> = T & Error; // workaround for Microsoft/TypeScript#23689

它会产生一个错误,错误会更好一些,例如()=>Promise<void> is not assignable to ["Expected", 3, "parameters, got", 0]。希望看到这些的人会有所提示。


好的,我们来测试一下!

class UserController implements MeetsConstraints<UserController> {

  public okayStringProp = "a"; // okay
  public okayObjectProp = { a: 1 }; // okay
  public okayArrayProp = [1, 2, 3]; // okay

  public async okayMethod(req: Request, res: Response, next: NextFunction) { } // okay

  public badMethod1() { return 17; } // error! 
  //     ~~~~~~~~~~ <-- number is not Promise<void>;

  public async badMethod2() { return 17; } // error!
  //           ~~~~~~~~~~ <-- number is not void

  public async badMethod3() { } // error!
  //           ~~~~~~~~~~ <-- error message is the best I can do here:
  // () => Promise<void>' is not assignable to type '["Expected", 3, "parameters, got", 0]'

  public async badMethod4(username: string) { } // error!
  //           ~~~~~~~~~~ <-- Request not assignable to string

  public async badMethod5(x: Request, y: Response, z: NextFunction, oops: number) {} // err!
  //           ~~~~~~~~~~ <-- 
  // (x: Request, y: Response, z: NextFunction, oops: number) => Promise<void>' 
  // is not assignable to type 'AcceptableMethod'

  public async signup(username: any, password: any) { } // error!
  //           ~~~~~~ <-- error message is the best I can do here:
  // ["Expected", 3, "parameters, got", 2]

}

如您所见,允许所有非方法属性,并允许所需的方法签名。所有其他方法都会给您带来某种类型的错误。

好的,希望能有所帮助。祝你好运!

Link to code

答案 1 :(得分:1)

@jcalz方法的另一种方法是通过在两个方向上进行类型检查来检查该方法是否完全相同:

type AcceptableMethod = (req: Request, res: Response, next: NextFunction) => Promise<void>;
type NextFunction = { noIdeaWhatYouWantHere(): void };

type MeetsConstraints<T> = {
  [K in keyof T]: T[K] extends Function 
    ? AcceptableMethod extends T[K] ? AcceptableMethod : never
    : T[K]
}

这里的窍门是在AcceptableMethod extends T[K]中,如果该方法的参数太少,它将失败(转到never)。

这里是获得所有错误的相同测试,但是错误消息更严重。

class UserController implements MeetsConstraints<UserController> {

  public okayStringProp = "a";
  public okayObjectProp = { a: 1 };
  public okayArrayProp = [1, 2, 3];

  public async okayMethod(req: Request, res: Response, next: NextFunction) { } // okay

  public badMethod1() { return 17; } // error! 
  //     ~~~~~~~~~~ <-- Type '() => number' is not assignable to type 'never'.

  public async badMethod2() { return 17; } // error!
  //           ~~~~~~~~~~ <-- Type '() => Promise<number>' is not assignable to type 'never'.

  public async badMethod3() { } // error!
  //           ~~~~~~~~~~ <-- Type '() => Promise<void>' is not assignable to type 'never'.

  public async badMethod4(username: string) { } // error!
  //           ~~~~~~~~~~ <-- Type '(username: string) => Promise<void>' is not assignable to type 'never'.

  public async badMethod5(x: Request, y: Response, z: NextFunction, oops: number) { } // error!
  //           ~~~~~~~~~~ <--Type '(x: Request, y: Response, z: NextFunction, oops: number) => Promise<void>' is not assignable to type 'AcceptableMethod'.

  public async signup(username: any, password: any) { } // error!
  //           ~~~~~~ <-- Type '(username: any, password: any) => Promise<void>' is not assignable to type 'never'.

}