文章

TypeScript元编程实现对象函数参数类型检查

TypeScript元编程实现对象函数参数类型检查

环境配置

TypeScript配置:

1
2
3
4
5
6
7
8
9
10
// tsconfig.json
{
    "compilerOptions": {
        "target": "ESNext",
				// 支持装饰器语法
        "experimentalDecorators": true,
				// 支持元数据
        "emitDecoratorMetadata": true
    }
}

Reflect-metadata 获取元数据

使用装饰器+Reflect-metadata,能获取元数据。这属于元编程。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import "reflect-metadata";

/**
 * 3种常见的从装饰器上元信息
 */
export function metadataDecorator(target: any, propertyKey: string) {
    console.log(">>>");
    console.log(Reflect.getMetadata("design:type", target, propertyKey));
    console.log(Reflect.getMetadata("design:paramtypes", target, propertyKey));
    console.log(Reflect.getMetadata("design:returntype", target, propertyKey));
}

export class Duty {
    private readonly name: string;
    private readonly createTime: number;
    constructor() {
        this.name = "duty info";
        this.createTime = Date.now();
    }
}

export class Runner {
    @metadataDecorator
    private readonly version: string;

    constructor() {
        this.version = "1.0.0";
    }

    @metadataDecorator
    run(duty: Duty): string {
        return "";
    }
}

上述代码的执行结果是:

1
2
3
4
5
6
7
8
>>>
[Function: String]
undefined
undefined
>>>
[Function: Function]
[ [Function: Duty] ]
[Function: String]

可以看出来:

  • design:type 代表成员类型
  • design:paramtypes 代表函数入参类型
  • design:returntype 代表函数返回类型

实现函数参数类型检查装饰器

实现一个用于检查函数的参数类型的装饰器:@paramTypeDecorator 。被装饰的函数,无须在函数内部显式的检查数据类型。

此装饰器的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const map = new Map();
export function paramTypeDecorator(
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
) {
    const paramTypes = Reflect.getMetadata(
        "design:paramtypes",
        target,
        propertyKey
    ); // 获得函数参数类型
    map.set(propertyKey, paramTypes); // 保存函数的参数类型

    const originalFunc = descriptor.value;
    // 修改函数的模型行为,在运行前,读取函数的参数类型,并且进行检查
    descriptor.value = function (...args: any[]) {
        const paramTypes = map.get(propertyKey);
        for (let i = 0; i < args.length; ++i) {
            if (!(args[i] instanceof paramTypes[i])) {
                throw new TypeError(
                    `Params[${i}] type should be ${paramTypes[i]?.name}`
                );
            }
        }
        const result = originalFunc.call(this, ...args);
        return result;
    };
}

看下在Runner中的使用效果:

1
2
3
4
5
6
7
8
9
10
11
export class Runner {
    // ... 其它部分和上面的Runner类似

    @paramTypeDecorator
    addDuty(duty: Duty) {}
}

const runner = new Runner();
const duty = new Duty();
// 能通过参数类型检查
runner.addDuty(duty);

这段代码会正常运行,类型检查成功。如果最后一句换成:runner.addDuty({} as any) ,那么会报错,输出:TypeError: Params[0] type should be Duty

本文由作者按照 CC BY 4.0 进行授权