最近在学习 TypeScript 5.x 中的一些新特性,其中一个比较大的更新就是 stage 3 的装饰器实现,发现本文对装饰器有比较深入的认知,故手译并分享之 ~
《2022 年开发者生态系统状况》(State of Developer Ecosystem 2022)将 TypeScript 评为增长最快的编程语言。这并不难理解。这种 JavaScript 的流行超集提供了类型检查、枚举和其他增强功能。但是,TypeScript 引入的那些期待已久的功能往往还不是 JavaScript 所依赖的 ECMAScript 标准的一部分。
其中一个例子就是今年发布的 TypeScript 5.0 重新引入了装饰器;装饰器是一种元编程技术,在其他编程语言中也能找到。如果您是有意使用新的官方 TypeScript 装饰器的应用程序开发人员或库作者,您需要采用新语法并了解新旧功能集之间的差异。API 之间的差异非常大,旧的装饰器不可能立即与新语法配合使用。
在本文中,我们将回顾在 TypeScript 中使用装饰器的历史,讨论与 TypeScript 5.0 中装饰器相关的优势,使用现代装饰器进行演示,并探讨如何重构现有装饰器。
注意: 在 TypeScript 5.0 中,所有 API 都发生了广泛的变化;本文将重点讨论类方法装饰器。
TypeScript 装饰器的历史
装饰器(Decorators)是一项功能,它使开发人员能够快速为类、类属性和类方法添加功能,从而减少模板。TypeScript 首次引入装饰器时,并未遵循 ECMAScript 规范。这对开发人员来说并不是件好事,因为理想情况下,任何 JavaScript 编译器生成的代码都应符合 Web 标准!
使用装饰器需要设置 –experimentalDecorators 实验编译器标志。一些流行的 TypeScript 库,如 type-graphql 和 inversify,都依赖于这种实现。
下面是一个简单的类方法装饰器示例,展示了新语法增强的人体工程学特性:
function debugMethod(_target: unknown, memberName: string, propertyDescriptor: PropertyDescriptor) {
return {
get() {
const wrapperFunction = (...arguments_: unknown[]) => {
const now = new Date(Date.now());
console.log('start time', now.toISOString());
propertyDescriptor.value.apply(this, arguments_);
const end = new Date(Date.now());
console.log('end time', end.toISOString());
};
Object.defineProperty(this, memberName, {
value: wrapperFunction,
configurable: true,
writable: true,
});
return wrapperFunction;
},
};
}
class ComplexClass {
@debugMethod
public complexMethod(a: number): void {
console.log("DOING COMPLEX STUFF!");
}
}
在上述代码中,我们可以看到 debugMethod
装饰器使用 Object.defineProperty
重载了类方法属性,但总的来说,代码并不容易理解。而且,参数不是类型安全的,这限制了我们在 wrapperFunction
内部的安全性。此外,如果在无效用例(如类属性)上使用此装饰器,编译器也不会失败。
我们可以使用 TypeScript 泛型来实现类型安全,但 TypeScript 不会推断泛型,这使得使用泛型非常麻烦。因此,编写复杂的装饰器非常困难,因为用户可能会在其中输入未知的值。
现代版本的装饰器将在 TypeScript 5.0 中正式推出,它不再需要编译器标志,并遵循 ECMAScript Stage-3 的官方提议。除了遵循 ECMAScript 标准的稳定实现外,装饰器现在还能与 TypeScript 类型系统无缝协作,实现比原始版本更强大的功能。
通过 TypeScript 5.0 中装饰器的新实现,这些方面都得到了极大的改进。让我们一起来看看。
TypeScript 5.0 中的装饰器
TypeScript 5.0 提供了更好的人体工学设计、更高的类型安全性等。下面是一个重载类方法的 TypeScript 5.0 装饰器的类似示例:
function debugMethod(originalMethod: any, _context: any) {
function replacementMethod(this: any, ...args: any[]) {
const now = new Date(Date.now());
console.log('start time', now.toISOString());
const result = originalMethod.call(this, ...args);
const end = new Date(Date.now());
console.log('end time', end.toISOString());
return result;
}
return replacementMethod;
}
class ComplexClass {
@debugMethod
complexMethod(a: number): void {
console.log("DOING STUFF!");
}
}
注:要在 在线 playground 中试用 TypeScript,只需将版本切换为 “nightly “或 “5.0”。
有了新的实现,现在只需返回函数即可替代它,而无需使用 Object.defineProperty。这使得装饰器更易于实现和理解。在改进的同时,我们还要使其完全类型安全:
function debugMethod<TThis, TArgs extends [string, number], TReturn extends number>(
originalMethod: Function,
context: ClassMethodDecoratorContext<TThis, (this: TThis, ...args: TArgs) => TReturn>
) {
function replacementMethod(this: TThis, a: TArgs[0], b: TArgs[1]): TReturn {
const now = new Date(Date.now());
console.log('start time', now.toISOString());
const result = originalMethod.call(this, a, b);
const end = new Date(Date.now());
console.log('end time', end.toISOString());
return result;
}
return replacementMethod;
}
TypeScript 5.0 中的装饰器功能得到了极大改进,现在支持以下功能:
- 使用泛型来键入方法的参数和返回值;方法必须接受一个字符串和一个数字(
TArgs
),并返回一个数字(TReturn)。 - 将
originalMethod
作为函数键入 - 使用
ClassMethodDecoratorContext
内置辅助类型;该类型适用于所有装饰器类型
我们可以通过检查错误使用时的错误,来测试我们的装饰器是否真正做到了类型安全:
现在,让我们看看新的 TypeScript 5.0 装饰器的实际用例。
装饰器工厂实例
我们可以使用 TypeScript 5.0 装饰器中提供的类型安全性来创建返回装饰器的函数,也就是装饰器工厂。装饰器工厂允许我们通过在工厂中传递一些参数来定制装饰器的行为。
在我们的演示中,我们将创建一个装饰器工厂,它可以根据自身参数更改类方法参数。这可以通过 TypeScript 类型三元操作符来实现。我们的示例受到了 NestJS
等 REST API 框架的启发。
我们将模块命名为 rest-framework
。让我们先用 ts-node
创建一个空白的 TypeScript 项目:
$ mkdir rest-framework
$ cd rest-framework
$ npm init -y
$ npm install -D typescript@5.0.4 @types/node ts-node
$ touch index.ts
$ echo "console.log('Hello, world!');" > index.ts
接下来,我们将在 package.json
中定义构建和运行项目的脚本:
{
// ...
"scripts": {
"build": "tsc",
"start": "ts-node index.ts"
}
}
让我们运行 npm start
看看它的运行情况:
$ npm start
Hello, world!
现在,让我们来定义我们的类型:
interface RouteOptionsAuthEnabled {
auth: true;
}
interface RouteOptionsAuthDisabled {
auth: false;
}
type RouteArguments = [string] | [];
type RouteDecorator<TThis, TArgs extends RouteArguments> = (
originalMethod: Function,
context: ClassMethodDecoratorContext<
TThis,
(this: TThis, ...args: TArgs) => string
>
) => void;
接下来,让我们定义工厂装饰器:
function Route<
TThis,
// The user can enable or disable auth
TOptions extends RouteOptionsAuthEnabled | RouteOptionsAuthDisabled
>(
options: TOptions
): RouteDecorator<
TThis,
// Do not accept a function that uses a string for an argument if auth is disabled
TOptions extends RouteOptionsAuthEnabled ? [string] : []
> {
return <TThis>(
target: (
this: TThis,
...args: TOptions extends RouteOptionsAuthEnabled ? [string] : []
) => string,
context: ClassMethodDecoratorContext<
TThis,
(
this: TThis,
...args: TOptions extends RouteOptionsAuthEnabled ? [string] : []
) => string
>
) => {};
}
现在,我们有了一个路由装饰器,它可以根据用户的选项改变类方法的参数类型。
让我们创建一个路由类示例作为测试用例:
class Controller {
@Route({ auth: true })
get(authHeaderValue: string): string {
console.log("get http method handled!");
return "response";
}
@Route({ auth: false })
post(): string {
console.log("post http method handled!");
return "response";
}
}
我们可以看到,如果我们尝试在 post
路由中使用 authHeaderValue
,TypeScript 就会编译失败:
装饰器工厂用例是一个简单的例子,但它展示了类型安全装饰器的强大功能。
重构现有装饰器
如果您正在使用现有的 TypeScript 装饰器,则需要重构以使用 API 并利用其诸多优势。基本的装饰器可以很容易地重构为新的装饰器,但两者之间的差异很大,高级用例需要花费大量精力。
为了达到最佳效果,请按照以下步骤重构现有的装饰器:
- 为装饰器编写单元测试
- 删除或伪造
experimentalDecorators TypeScript
编译器标志 - 阅读这份关于新提案如何工作的详尽摘要
- 了解现代装饰器的局限性(我们将在本文稍后部分详细介绍这一点)
- 重写不使用任何类型的装饰器,并使用 any 代替所有类型
- 确保单元测试通过
- 添加类型
了解现代装饰公司的局限性
对于 TypeScript 开发人员来说,现代装饰器的实现是个好消息,但也缺少一些值得注意的功能。首先,它不支持装饰方法参数。这属于提案的规范范围,因此希望它能被包含在最终规范中。它的缺失是值得注意的,因为流行的库,如 type-graphql
,在编写解析器等重要方面都使用了这一功能:
@Query(returns => Recipe)
async recipe(@Arg("recipeId") recipeId: string) {
return this.recipeRepository.findOneById(recipeId);
}
其次,TypeScript 5.0 无法发射装饰器元数据。因此,它无法与 Reflect API 集成,也无法使用 reflect-metadata npm 软件包。
第三,以前用于访问和修改给定装饰器元数据的 --emitDecoratorMetadata
标志已不再受支持。遗憾的是,目前还没有真正的办法通过在运行时获取元数据来实现相同的功能。有一些情况是可以重构的。例如,让我们定义一个装饰器,在运行时验证函数的参数类型:
function validateParameterType(target: any, propertyKey: string | symbol): void {
const methodParameterTypes: (string | unknown)[] =
Reflect.getMetadata("design:paramtypes", target, propertyKey) ?? [];
const firstParameterType = methodParameterTypes[0];
if (typeof firstParameterType !== "string") {
throw new TypeError("First parameter must be a string");
}
}
我们可以利用 TypeScript 5.0 提供的改进的类型安全性实现类似功能。我们只需添加我们要装饰的方法的参数,就像这样:
function debugMethod<TThis, TArgs extends [string], TReturn>(
) {
// ...
}
理论上,我们可以使用这种方法来重构依赖于从 Reflect
获取类型的装饰器:design:type
、design:paramtypes
和 design:returntype
。这是写装饰器的另一种方法;它不是简单的重构,因为它需要使用 TypeScript 类型推理来重构类型的获取和验证方式。
结论
TypeScript 5.0 中新的装饰器实现遵循了官方的 ECMAScript Stage-3
提议,现在是类型安全的,因此更容易实现和理解。不过,它还缺少一些值得注意的功能,例如对装饰方法参数的支持和发射装饰器元数据的功能。
基本的装饰器可以很容易地重构为 TypeScript 5.0 版本,但高级用例则需要更多的努力。开发人员可以重构现有的装饰器,以使用新的 API 并利用相关优势。他们可以减少对外部库的依赖,将来重构代码的可能性也会降低。对 TypeScript 的装饰器实现所做的这些更改有利于更广泛的生态系统,但社区采用还需要一些时间。