TypeScript 的类型系统在编译时提供了强大的类型安全,但在运行时,这些类型信息会丢失。当需要验证传入函数的动态数据(如 API 请求体)时,我们通常需要编写大量重复的 typeof 或 instanceof 检查代码。TypeScript 装饰器结合 reflect-metadata 库,可以在运行时存储和访问类型信息,从而实现优雅的、声明式的类型检查。
本文将指导你如何使用装饰器元数据,在运行时验证对象的结构和类型。
1. 准备工作
首先,你需要一个 TypeScript 项目并安装必要的依赖。
-
初始化项目
在你的项目目录中打开终端,运行以下命令创建一个新的 Node.js 项目。npm init -y -
安装依赖
安装 TypeScript 编译器和reflect-metadata库。npm install typescript reflect-metadata --save-dev -
配置 TypeScript
在项目根目录下创建tsconfig.json文件,并启用装饰器和元数据发射。{ "compilerOptions": { "target": "ES2020", "module": "commonjs", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "experimentalDecorators": true, "emitDecoratorMetadata": true } }
2. 核心概念:reflect-metadata
reflect-metadata 库为 JavaScript 的 Reflect API 添加了元数据操作能力。通过装饰器,我们可以将类型信息附加到类的属性、方法参数或返回值上。
Reflect.defineMetadata(key, value, target, propertyKey?): 在目标对象上定义元数据。Reflect.getMetadata(key, target, propertyKey?): 从目标对象上获取元数据。
TypeScript 编译器在 emitDecoratorMetadata 启用时,会自动在装饰器中注入类型信息,并使用 design:type、design:paramtypes 和 design:returntype 作为键名。
3. 定义数据模型和装饰器
我们将创建一个 User 类作为数据模型,并定义一个 @Type 装饰器来标记属性的类型。
-
创建数据模型
创建一个models.ts文件,定义User类。注意,我们暂时不使用装饰器。// models.ts export class User { id: number; name: string; email: string; } -
创建类型装饰器
创建一个decorators.ts文件,定义@Type装饰器。这个装饰器将把属性的类型信息存储起来。// decorators.ts import 'reflect-metadata'; /** * 用于标记类属性类型的装饰器。 * @param target 类的原型对象 * @param propertyKey 属性名 */ export function Type(target: any, propertyKey: string) { // 获取属性的设计时类型 const type = Reflect.getMetadata('design:type', target, propertyKey); // 将类型信息存储为元数据 Reflect.defineMetadata(`custom:type:${propertyKey}`, type, target); } ``` --- ### 4. 实现运行时验证器 现在,我们编写一个 `validate` 函数,它将使用存储的元数据来检查一个对象是否符合其类的定义。 1. **创建验证器** 在 `validators.ts` 文件中,实现 `validate` 函数。 ```typescript // validators.ts import 'reflect-metadata'; import { Type } from './decorators'; /** * 验证一个对象是否符合指定类的结构。 * @param instance 要验证的实例对象 * @param target 类的构造函数 * @returns 如果验证通过返回 true,否则返回 false */ export function validate<T>(instance: T, target: new () => T): boolean { const prototype = target.prototype; // 遍历目标类的所有自有属性 for (const key of Object.getOwnPropertyNames(prototype)) { if (key === 'constructor') continue; // 获取存储的类型元数据 const expectedType = Reflect.getMetadata(`custom:type:${key}`, prototype); if (!expectedType) continue; // 如果没有装饰器,跳过 const actualValue = instance[key as keyof T]; // 检查实际值的类型是否与期望类型匹配 if (typeof actualValue !== expectedType.name.toLowerCase()) { console.error(`Validation failed for property '${key}': Expected type '${expectedType.name}', but got '${typeof actualValue}'.`); return false; } } return true; } ``` --- ### 5. 整合应用 最后,我们创建一个 `index.ts` 文件来演示如何使用这些组件。 1. **应用装饰器并验证** 在 `index.ts` 中,我们将 `@Type` 装饰器应用到 `User` 类的属性上,并测试验证逻辑。 ```typescript // index.ts import { User } from './models'; import { Type } from './decorators'; import { validate } from './validators'; // 将 @Type 装饰器应用到 User 类的属性上 class User { @Type id: number; @Type name: string; @Type email: string; } // 创建一个符合类型的有效对象 const validUser = { id: 1, name: 'Alice', email: 'alice@example.com' }; // 创建一个类型不匹配的无效对象 const invalidUser = { id: 'one', // 应该是 number,但却是 string name: 'Bob', email: 'bob@example.com' }; console.log('--- 验证有效对象 ---'); const isValid1 = validate(validUser, User); console.log(`Validation result: ${isValid1}`); console.log('\n--- 验证无效对象 ---'); const isValid2 = validate(invalidUser, User); console.log(`Validation result: ${isValid2}`); ``` 2. **运行代码** 在终端中,使用 TypeScript 编译器编译并运行代码。 ```bash npx tsc node index.js ``` 你将看到以下输出: ``` --- 验证有效对象 --- Validation result: true --- 验证无效对象 --- Validation failed for property 'id': Expected type 'Number', but got 'string'. Validation result: false ``` --- ### 6. 扩展应用:处理复杂类型 上述示例仅处理了基本类型。对于数组或嵌套对象等复杂类型,我们需要更精细的元数据键管理。 1. **修改装饰器以支持复杂类型** 修改 `decorators.ts` 中的 `@Type` 装饰器,使其能够处理构造函数(如数组或另一个类)。 ```typescript // decorators.ts (修改后) import 'reflect-metadata'; export function Type(target: any, propertyKey: string) { // 获取属性的设计时类型,它可能是基本类型或构造函数 const type = Reflect.getMetadata('design:type', target, propertyKey); // 使用更具体的键名来存储类型信息 Reflect.defineMetadata(`custom:type:${propertyKey}`, type, target); } -
修改验证器以处理复杂类型
更新validators.ts中的validate函数,使其能够检查构造函数类型。// validators.ts (修改后) import 'reflect-metadata'; export function validate<T>(instance: T, target: new () => T): boolean { const prototype = target.prototype; for (const key of Object.getOwnPropertyNames(prototype)) { if (key === 'constructor') continue; const expectedType = Reflect.getMetadata(`custom:type:${key}`, prototype); if (!expectedType) continue; const actualValue = instance[key as keyof T]; // 如果期望类型是构造函数(如 Array 或另一个类) if (typeof expectedType === 'function') { // 对于 Array,检查是否是数组 if (expectedType === Array) { if (!Array.isArray(actualValue)) { console.error(`Validation failed for property '${key}': Expected an array.`); return false; } } // 对于自定义类,可以递归验证(此处省略递归逻辑以保持示例简洁) } else { // 处理基本类型 if (typeof actualValue !== expectedType.name.toLowerCase()) { console.error(`Validation failed for property '${key}': Expected type '${expectedType.name}', but got '${typeof actualValue}'.`); return false; } } } return true; } -
测试复杂类型
修改index.ts来测试一个包含数组的模型。// index.ts (修改后) import { Type } from './decorators'; import { validate } from './validators'; class Post { @Type title: string; @Type content: string; } class User { @Type id: number; @Type name: string; @Type posts: Post[]; // 这是一个复杂类型:Post 数组 } const userWithPosts = { id: 1, name: 'Charlie', posts: [ { title: 'First Post', content: 'Hello World' }, { title: 'Second Post', content: 'Another one' } ] }; const invalidUserWithPosts = { id: 1, name: 'Dave', posts: 'not an array' // 类型错误 }; console.log('--- 验证包含数组的对象 ---'); console.log('验证有效对象:'); validate(userWithPosts, User); console.log('\n验证无效对象:'); validate(invalidUserWithPosts, User);
通过这种方式,你可以构建一个灵活的运行时类型验证系统,它利用 TypeScript 的类型系统,在运行时提供强大的数据验证能力,减少样板代码并提高代码的可维护性。

暂无评论,快来抢沙发吧!