MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript属性装饰器实战:数据验证与默认值设置

2021-05-092.8k 阅读

TypeScript 属性装饰器基础概念

在深入探讨 TypeScript 属性装饰器在数据验证与默认值设置方面的实战应用之前,我们先来回顾一下属性装饰器的基本概念。属性装饰器是 TypeScript 装饰器中的一种类型,它主要用于对类的属性进行元编程操作。属性装饰器的语法形式为在属性声明之前使用 @ 符号加上装饰器函数的调用。

属性装饰器表达式会在运行时当作函数被调用,并且会传入两个参数:

  1. 对于静态成员,它是类的构造函数;对于实例成员,它是类的原型对象:这个参数可以让我们在装饰器中访问类的相关信息,比如可以通过构造函数来扩展类的静态属性,或者通过原型对象来扩展实例属性。
  2. 属性的名称:通过这个参数,我们可以精确地定位到被装饰的具体属性,从而针对不同的属性进行不同的操作。

例如,下面是一个简单的属性装饰器示例:

function logProperty(target: any, propertyKey: string) {
    let value = target[propertyKey];

    const getter = () => {
        console.log(`Get: ${propertyKey} => ${value}`);
        return value;
    };

    const setter = (newValue: any) => {
        console.log(`Set: ${propertyKey} => ${newValue}`);
        value = newValue;
    };

    if (delete target[propertyKey]) {
        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true
        });
    }
}

class MyClass {
    @logProperty
    myProperty: string = 'default value';
}

const obj = new MyClass();
console.log(obj.myProperty); 
obj.myProperty = 'new value'; 

在这个例子中,logProperty 装饰器对 MyClass 类的 myProperty 属性进行了拦截,通过自定义的 gettersetter 函数,在访问和修改属性值时打印出相关的日志信息。

数据验证的需求背景

在实际的前端开发中,数据验证是一个至关重要的环节。无论是从用户输入获取的数据,还是从后端接口返回的数据,都需要进行严格的验证,以确保数据的准确性、完整性和安全性。例如,在一个用户注册表单中,用户输入的邮箱格式必须正确,密码长度必须符合规定,年龄必须是一个合理的数字等。

传统的数据验证方式通常是在业务逻辑代码中进行,比如在表单提交时,对各个输入字段进行逐一验证。这种方式虽然可行,但存在一些弊端:

  1. 代码冗余:在多个地方对相同类型的数据进行验证时,会出现大量重复的验证代码。例如,在多个表单中都需要验证邮箱格式,每个表单都要写一遍验证邮箱的逻辑。
  2. 维护困难:如果验证规则发生变化,比如邮箱格式的正则表达式需要更新,那么就需要在所有涉及邮箱验证的地方进行修改,容易遗漏且工作量大。
  3. 侵入业务逻辑:验证代码和业务逻辑代码混合在一起,使得业务逻辑代码变得复杂,可读性和可维护性降低。

使用属性装饰器进行数据验证,可以有效地解决这些问题。它将验证逻辑从业务逻辑中分离出来,以一种更加优雅和可复用的方式对数据进行验证。

使用属性装饰器实现数据验证

  1. 基本数据类型验证
    • 数字类型验证:假设我们有一个类,其中某个属性必须是大于 0 的数字。我们可以编写如下的属性装饰器来实现验证:
function numberGreaterThanZero(target: any, propertyKey: string) {
    let value: number;

    const getter = () => value;

    const setter = (newValue: any) => {
        if (typeof newValue!== 'number' || newValue <= 0) {
            throw new Error(`${propertyKey} must be a number greater than zero`);
        }
        value = newValue;
    };

    if (delete target[propertyKey]) {
        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true
        });
    }
}

class Product {
    @numberGreaterThanZero
    price: number;

    constructor(price: number) {
        this.price = price;
    }
}

try {
    const product = new Product(10); 
    console.log(product.price); 
    const invalidProduct = new Product(-5); 
} catch (error) {
    console.error(error.message); 
}

在这个例子中,numberGreaterThanZero 装饰器对 Product 类的 price 属性进行了验证。当设置 price 属性值时,如果值不是大于 0 的数字,就会抛出一个错误。

  • 字符串长度验证:对于字符串类型的属性,我们可能需要验证其长度。比如,一个用户名属性,长度必须在 3 到 20 个字符之间。
function stringLengthRange(min: number, max: number) {
    return function (target: any, propertyKey: string) {
        let value: string;

        const getter = () => value;

        const setter = (newValue: any) => {
            if (typeof newValue!== 'string' || newValue.length < min || newValue.length > max) {
                throw new Error(`${propertyKey} length must be between ${min} and ${max}`);
            }
            value = newValue;
        };

        if (delete target[propertyKey]) {
            Object.defineProperty(target, propertyKey, {
                get: getter,
                set: setter,
                enumerable: true,
                configurable: true
            });
        }
    };
}

class User {
    @stringLengthRange(3, 20)
    username: string;

    constructor(username: string) {
        this.username = username;
    }
}

try {
    const user = new User('john'); 
    console.log(user.username); 
    const invalidUser = new User('a'); 
} catch (error) {
    console.error(error.message); 
}

这里的 stringLengthRange 装饰器工厂函数接收 minmax 作为参数,返回一个实际的属性装饰器。该装饰器对 User 类的 username 属性进行字符串长度验证。 2. 复杂数据结构验证

  • 数组元素类型验证:当类的属性是一个数组,并且数组中的元素必须是特定类型时,我们可以这样实现验证。例如,一个包含数字的数组,数组中的每个元素都必须是大于 10 的数字。
function arrayElementsGreaterThanTen(target: any, propertyKey: string) {
    let value: number[];

    const getter = () => value;

    const setter = (newValue: any) => {
        if (!Array.isArray(newValue) || newValue.some((num: number) => typeof num!== 'number' || num <= 10)) {
            throw new Error(`${propertyKey} must be an array of numbers greater than ten`);
        }
        value = newValue;
    };

    if (delete target[propertyKey]) {
        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true
        });
    }
}

class DataCollection {
    @arrayElementsGreaterThanTen
    numbers: number[];

    constructor(numbers: number[]) {
        this.numbers = numbers;
    }
}

try {
    const collection = new DataCollection([15, 20]); 
    console.log(collection.numbers); 
    const invalidCollection = new DataCollection([5, 10]); 
} catch (error) {
    console.error(error.message); 
}
  • 对象属性验证:对于一个包含特定属性且属性类型符合要求的对象,也可以进行验证。比如,一个表示地址的对象,必须包含 city(字符串类型)和 postalCode(数字类型)属性。
function validateAddress(target: any, propertyKey: string) {
    let value: { city: string; postalCode: number };

    const getter = () => value;

    const setter = (newValue: any) => {
        if (typeof newValue!== 'object' ||!('city' in newValue) || typeof newValue.city!=='string' ||!('postalCode' in newValue) || typeof newValue.postalCode!== 'number') {
            throw new Error(`${propertyKey} must be an object with city (string) and postalCode (number) properties`);
        }
        value = newValue;
    };

    if (delete target[propertyKey]) {
        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true
        });
    }
}

class Person {
    @validateAddress
    address: { city: string; postalCode: number };

    constructor(address: { city: string; postalCode: number }) {
        this.address = address;
    }
}

try {
    const person = new Person({ city: 'New York', postalCode: 10001 }); 
    console.log(person.address); 
    const invalidPerson = new Person({ city: 'LA' }); 
} catch (error) {
    console.error(error.message); 
}
  1. 结合正则表达式的验证 在验证字符串类型的数据时,正则表达式是一个非常强大的工具。例如,验证邮箱格式、电话号码格式等。我们可以编写一个通用的基于正则表达式的属性装饰器。
function regexValidator(regex: RegExp, errorMessage: string) {
    return function (target: any, propertyKey: string) {
        let value: string;

        const getter = () => value;

        const setter = (newValue: any) => {
            if (typeof newValue!=='string' ||!regex.test(newValue)) {
                throw new Error(`${propertyKey}: ${errorMessage}`);
            }
            value = newValue;
        };

        if (delete target[propertyKey]) {
            Object.defineProperty(target, propertyKey, {
                get: getter,
                set: setter,
                enumerable: true,
                configurable: true
            });
        }
    };
}

const emailRegex = /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/;

class Contact {
    @regexValidator(emailRegex, 'Invalid email format')
    email: string;

    constructor(email: string) {
        this.email = email;
    }
}

try {
    const contact = new Contact('john@example.com'); 
    console.log(contact.email); 
    const invalidContact = new Contact('invalid-email'); 
} catch (error) {
    console.error(error.message); 
}

在这个例子中,regexValidator 装饰器工厂函数接收一个正则表达式和错误信息作为参数,返回的装饰器对字符串类型的属性进行正则表达式匹配验证。

默认值设置的需求背景

在前端开发中,为属性设置默认值是一个常见的需求。默认值可以确保在属性未被显式赋值时,程序仍能正常运行。例如,在一个配置类中,某些配置项可能有默认的设置,如果用户没有提供自定义的配置,就使用默认值。

传统的设置默认值的方式通常是在属性声明时直接赋值,或者在类的构造函数中进行赋值。然而,这种方式在一些情况下存在局限性:

  1. 缺乏灵活性:如果默认值的计算需要依赖一些外部条件,比如根据环境变量来决定默认值,在属性声明或构造函数中直接赋值就不太方便。
  2. 代码重复:当多个类有相似的属性且需要相同的默认值设置逻辑时,会出现重复代码。例如,多个数据模型类都有一个表示创建时间的属性,都需要设置默认值为当前时间。

使用属性装饰器来设置默认值,可以提供一种更加灵活和可复用的方式,将默认值设置逻辑从类的核心代码中分离出来。

使用属性装饰器设置默认值

  1. 简单数据类型默认值设置
    • 数字类型默认值:假设我们有一个表示商品折扣的属性,默认折扣为 0(即无折扣)。
function setDefaultDiscount(target: any, propertyKey: string) {
    const defaultValue = 0;

    let value = target[propertyKey];

    const getter = () => value === undefined? defaultValue : value;

    const setter = (newValue: any) => {
        value = newValue;
    };

    if (delete target[propertyKey]) {
        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true
        });
    }
}

class Product {
    @setDefaultDiscount
    discount: number;

    constructor(discount?: number) {
        this.discount = discount;
    }
}

const product = new Product(); 
console.log(product.discount); 
const discountedProduct = new Product(0.1); 
console.log(discountedProduct.discount); 

在这个例子中,setDefaultDiscount 装饰器为 Product 类的 discount 属性设置了默认值 0。当属性未被显式赋值时,通过 getter 函数返回默认值。

  • 字符串类型默认值:对于一个表示用户昵称的属性,如果用户没有设置昵称,我们可以设置默认昵称为 “Guest”。
function setDefaultNickname(target: any, propertyKey: string) {
    const defaultValue = 'Guest';

    let value = target[propertyKey];

    const getter = () => value === undefined? defaultValue : value;

    const setter = (newValue: any) => {
        value = newValue;
    };

    if (delete target[propertyKey]) {
        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true
        });
    }
}

class User {
    @setDefaultNickname
    nickname: string;

    constructor(nickname?: string) {
        this.nickname = nickname;
    }
}

const user = new User(); 
console.log(user.nickname); 
const namedUser = new User('John'); 
console.log(namedUser.nickname); 
  1. 复杂数据结构默认值设置
    • 数组类型默认值:比如一个表示用户收藏的商品列表的属性,默认情况下为空数组。
function setDefaultProductList(target: any, propertyKey: string) {
    const defaultValue: string[] = [];

    let value = target[propertyKey];

    const getter = () => value === undefined? defaultValue : value;

    const setter = (newValue: any) => {
        value = newValue;
    };

    if (delete target[propertyKey]) {
        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true
        });
    }
}

class User {
    @setDefaultProductList
    favoriteProducts: string[];

    constructor(favoriteProducts?: string[]) {
        this.favoriteProducts = favoriteProducts;
    }
}

const user = new User(); 
console.log(user.favoriteProducts); 
const userWithFavorites = new User(['Product1', 'Product2']); 
console.log(userWithFavorites.favoriteProducts); 
  • 对象类型默认值:对于一个表示用户设置的属性,默认设置可能包含一些默认的主题和语言选项。
function setDefaultUserSettings(target: any, propertyKey: string) {
    const defaultValue = {
        theme: 'light',
        language: 'en'
    };

    let value = target[propertyKey];

    const getter = () => value === undefined? defaultValue : value;

    const setter = (newValue: any) => {
        value = newValue;
    };

    if (delete target[propertyKey]) {
        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true
        });
    }
}

class User {
    @setDefaultUserSettings
    settings: { theme: string; language: string };

    constructor(settings?: { theme: string; language: string }) {
        this.settings = settings;
    }
}

const user = new User(); 
console.log(user.settings); 
const userWithSettings = new User({ theme: 'dark', language: 'zh' }); 
console.log(userWithSettings.settings); 
  1. 动态默认值设置 有时候,默认值的设置需要依赖一些外部条件,比如根据当前的时间、环境变量等。我们可以通过传递参数给装饰器工厂函数来实现动态默认值。
function setDefaultBasedOnTime(target: any, propertyKey: string) {
    const now = new Date();
    const hour = now.getHours();
    const defaultValue = hour < 12? 'Good morning' : 'Good afternoon';

    let value = target[propertyKey];

    const getter = () => value === undefined? defaultValue : value;

    const setter = (newValue: any) => {
        value = newValue;
    };

    if (delete target[propertyKey]) {
        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true
        });
    }
}

class Greeting {
    @setDefaultBasedOnTime
    message: string;

    constructor(message?: string) {
        this.message = message;
    }
}

const greeting = new Greeting(); 
console.log(greeting.message); 

在这个例子中,setDefaultBasedOnTime 装饰器根据当前时间来设置 message 属性的默认值。如果当前时间在中午 12 点之前,默认值为 “Good morning”,否则为 “Good afternoon”。

数据验证与默认值设置结合

在实际应用中,数据验证和默认值设置往往是相辅相成的。我们可以将两者结合起来,先进行数据验证,如果验证通过则设置属性值,如果未通过验证则使用默认值。

例如,对于一个表示用户年龄的属性,我们要求年龄必须是大于 0 且小于 120 的数字。如果用户输入的年龄不符合要求,则使用默认值 18。

function validateAndSetDefaultAge(target: any, propertyKey: string) {
    const defaultValue = 18;
    let value: number;

    const getter = () => value === undefined? defaultValue : value;

    const setter = (newValue: any) => {
        if (typeof newValue === 'number' && newValue > 0 && newValue < 120) {
            value = newValue;
        } else {
            console.warn(`${propertyKey} is invalid, using default value`);
            value = defaultValue;
        }
    };

    if (delete target[propertyKey]) {
        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true
        });
    }
}

class User {
    @validateAndSetDefaultAge
    age: number;

    constructor(age?: number) {
        this.age = age;
    }
}

const user1 = new User(25); 
console.log(user1.age); 
const user2 = new User(-5); 
console.log(user2.age); 
const user3 = new User(); 
console.log(user3.age); 

在这个例子中,validateAndSetDefaultAge 装饰器首先对传入的年龄值进行验证,如果验证通过则设置为属性值,否则使用默认值 18,并打印警告信息。

再比如,对于一个表示用户邮箱的属性,先验证邮箱格式是否正确,如果不正确则使用默认邮箱 “noreply@example.com”。

function validateAndSetDefaultEmail(target: any, propertyKey: string) {
    const defaultValue = 'noreply@example.com';
    const emailRegex = /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/;
    let value: string;

    const getter = () => value === undefined? defaultValue : value;

    const setter = (newValue: any) => {
        if (typeof newValue ==='string' && emailRegex.test(newValue)) {
            value = newValue;
        } else {
            console.warn(`${propertyKey} is invalid, using default value`);
            value = defaultValue;
        }
    };

    if (delete target[propertyKey]) {
        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true
        });
    }
}

class User {
    @validateAndSetDefaultEmail
    email: string;

    constructor(email?: string) {
        this.email = email;
    }
}

const user1 = new User('john@example.com'); 
console.log(user1.email); 
const user2 = new User('invalid-email'); 
console.log(user2.email); 
const user3 = new User(); 
console.log(user3.email); 

通过这种方式,我们可以在确保数据有效性的同时,提供合理的默认值,增强程序的健壮性。

在前端框架中的应用

  1. 在 React 中的应用 在 React 项目中,我们经常会使用类组件来管理状态和处理逻辑。使用属性装饰器进行数据验证和默认值设置可以让组件代码更加清晰和可维护。

例如,我们有一个 UserProfile 组件,接收 nameage 属性。

import React, { Component } from'react';

function validateStringLength(min: number, max: number) {
    return function (target: any, propertyKey: string) {
        let value: string;

        const getter = () => value;

        const setter = (newValue: any) => {
            if (typeof newValue!=='string' || newValue.length < min || newValue.length > max) {
                throw new Error(`${propertyKey} length must be between ${min} and ${max}`);
            }
            value = newValue;
        };

        if (delete target[propertyKey]) {
            Object.defineProperty(target, propertyKey, {
                get: getter,
                set: setter,
                enumerable: true,
                configurable: true
            });
        }
    };
}

function setDefaultAge(target: any, propertyKey: string) {
    const defaultValue = 18;

    let value = target[propertyKey];

    const getter = () => value === undefined? defaultValue : value;

    const setter = (newValue: any) => {
        value = newValue;
    };

    if (delete target[propertyKey]) {
        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true
        });
    }
}

class UserProfile extends Component<{
    @validateStringLength(3, 20)
    name: string;
    @setDefaultAge
    age: number;
}> {
    render() {
        const { name, age } = this.props;
        return (
            <div>
                <p>Name: {name}</p>
                <p>Age: {age}</p>
            </div>
        );
    }
}

const profile1 = <UserProfile name="John" age={25} />; 
const profile2 = <UserProfile name="J" age={-5} />; 
const profile3 = <UserProfile name="Jane" />; 

在这个例子中,validateStringLength 装饰器对 name 属性进行长度验证,setDefaultAge 装饰器为 age 属性设置默认值。这样可以在组件接收属性时就进行数据验证和默认值设置,避免在组件内部进行重复的验证和赋值逻辑。 2. 在 Vue 中的应用 在 Vue 项目中,我们可以在 Vue 组件的 data 函数返回的对象属性上使用属性装饰器。

首先,我们需要借助 vue - property - decorator 库来使用装饰器语法。假设我们有一个 ProductCard 组件。

import { Vue, Component, Prop } from 'vue - property - decorator';

function validatePrice(target: any, propertyKey: string) {
    let value: number;

    const getter = () => value;

    const setter = (newValue: any) => {
        if (typeof newValue!== 'number' || newValue <= 0) {
            throw new Error(`${propertyKey} must be a number greater than zero`);
        }
        value = newValue;
    };

    if (delete target[propertyKey]) {
        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true
        });
    }
}

function setDefaultStock(target: any, propertyKey: string) {
    const defaultValue = 0;

    let value = target[propertyKey];

    const getter = () => value === undefined? defaultValue : value;

    const setter = (newValue: any) => {
        value = newValue;
    };

    if (delete target[propertyKey]) {
        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true
        });
    }
}

@Component
export default class ProductCard extends Vue {
    @Prop()
    @validatePrice
    price: number;

    @Prop()
    @setDefaultStock
    stock: number;

    get productInfo() {
        return `Price: ${this.price}, Stock: ${this.stock}`;
    }
}

在这个 ProductCard 组件中,validatePrice 装饰器对 price 属性进行验证,setDefaultStock 装饰器为 stock 属性设置默认值。这样可以使 Vue 组件的属性管理更加规范和健壮。

注意事项与最佳实践

  1. 装饰器的兼容性 虽然 TypeScript 支持装饰器,但目前装饰器仍处于实验阶段,不同的运行环境对装饰器的支持程度可能有所不同。在实际项目中,需要注意目标运行环境是否支持装饰器,或者是否需要使用 Babel 等工具进行转译。
  2. 装饰器的性能影响 属性装饰器在运行时会对属性的访问和设置进行额外的操作,虽然在大多数情况下这种性能影响可以忽略不计,但在性能敏感的场景中,需要对装饰器的使用进行评估。尽量避免在频繁访问的属性上使用过于复杂的装饰器逻辑。
  3. 错误处理 在数据验证装饰器中,合理的错误处理非常重要。应该根据实际情况抛出有意义的错误信息,以便开发人员能够快速定位问题。同时,在默认值设置与验证结合的场景中,要考虑是否需要记录警告信息,以帮助调试。
  4. 代码组织与复用 为了提高代码的可维护性和复用性,应该将相关的装饰器函数进行合理的组织。可以按照功能模块或者数据类型来划分装饰器,例如将所有数字类型验证的装饰器放在一个文件中,字符串类型验证的放在另一个文件中。这样在需要使用时可以方便地导入和复用。

通过合理地使用 TypeScript 属性装饰器进行数据验证和默认值设置,我们可以提高前端代码的质量、可维护性和健壮性,使代码更加优雅和高效。无论是在小型项目还是大型企业级应用中,这种方式都能为开发带来诸多便利。