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

TypeScript正则表达式类型安全验证器

2021-12-214.2k 阅读

1. 正则表达式在编程中的重要性

正则表达式是一种强大的文本模式匹配工具,在各种编程语言中都被广泛应用。它能够帮助开发者快速地在字符串中查找、替换符合特定模式的文本。例如,在处理用户输入时,我们常常需要验证输入是否符合某种格式,像邮箱地址、电话号码、日期等。在Web开发中,验证用户输入的合法性是保障系统安全和稳定性的重要环节。

以邮箱地址验证为例,一个有效的邮箱地址需要满足特定的格式,如包含一个“@”符号,“@”符号前后都要有字符,且“@”符号后要有一个或多个“.”符号等。使用正则表达式,我们可以简洁地定义这样的匹配模式,并对用户输入进行验证。

2. TypeScript 中的类型系统基础

TypeScript 是 JavaScript 的超集,它为 JavaScript 添加了静态类型系统。在 TypeScript 中,类型声明可以帮助开发者在编译阶段发现许多潜在的错误,提高代码的可维护性和健壮性。

2.1 基本类型

TypeScript 拥有一系列基本类型,如 numberstringboolean 等。例如:

let num: number = 10;
let str: string = "hello";
let bool: boolean = true;

2.2 自定义类型

开发者还可以通过 interfacetype 关键字定义自定义类型。

interface User {
    name: string;
    age: number;
}
let user: User = { name: "John", age: 30 };

type Point = {
    x: number;
    y: number;
};
let point: Point = { x: 1, y: 2 };

3. 传统正则表达式验证的问题

在 JavaScript 中使用正则表达式进行验证时,虽然功能强大,但存在类型安全问题。例如,我们通常会这样验证邮箱地址:

function validateEmail(email) {
    const re = /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/;
    return re.test(email);
}

然而,在 TypeScript 项目中,如果我们直接使用这样的代码,编译器无法对 email 参数的类型进行有效的检查。如果在调用 validateEmail 函数时传入了非字符串类型的值,运行时才会报错,这不符合 TypeScript 强调的编译时类型检查的优势。

4. 构建 TypeScript 正则表达式类型安全验证器

4.1 定义类型安全的验证函数

为了解决上述问题,我们可以构建一个类型安全的正则表达式验证器。首先,我们定义一个通用的验证函数,它接受一个正则表达式和一个待验证的值,并返回一个布尔值表示验证结果。

function validate<T>(regex: RegExp, value: T): boolean {
    if (typeof value === "string") {
        return regex.test(value);
    }
    return false;
}

这里,我们使用了泛型 T 来表示待验证值的类型。在函数内部,我们首先检查 value 是否为字符串类型,如果是,则使用正则表达式进行测试。这样,当我们调用这个函数时,TypeScript 编译器能够对 value 的类型进行检查。

4.2 针对特定模式的验证函数封装

以邮箱验证为例,我们可以基于上述通用验证函数封装一个专门的邮箱验证函数。

const emailRegex = /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/;
function validateEmail(email: string): boolean {
    return validate(emailRegex, email);
}

现在,当我们调用 validateEmail 函数时,如果传入的不是字符串类型的值,TypeScript 编译器会在编译阶段报错,从而确保了类型安全。

5. 处理更复杂的验证场景

5.1 日期格式验证

假设我们要验证日期格式为“YYYY - MM - DD”,我们可以按照以下步骤进行。 首先,定义日期正则表达式。

const dateRegex = /^\d{4}-\d{2}-\d{2}$/;

然后,基于通用验证函数封装日期验证函数。

function validateDate(date: string): boolean {
    return validate(dateRegex, date);
}

5.2 电话号码验证

对于电话号码,假设我们的格式要求是“区号 - 号码”,区号为 3 位数字,号码为 8 位数字,我们可以这样实现。 定义电话号码正则表达式。

const phoneRegex = /^\d{3}-\d{8}$/;

封装电话号码验证函数。

function validatePhone(phone: string): boolean {
    return validate(phoneRegex, phone);
}

6. 结合表单验证的应用

在 Web 开发中,表单验证是一个常见的应用场景。假设我们有一个用户注册表单,包含邮箱、日期和电话号码字段。

6.1 HTML 表单结构

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF - 8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>User Registration</title>
</head>

<body>
    <form id="registrationForm">
        <label for="email">Email:</label>
        <input type="text" id="email" name="email"><br>
        <label for="date">Date of Birth:</label>
        <input type="text" id="date" name="date"><br>
        <label for="phone">Phone Number:</label>
        <input type="text" id="phone" name="phone"><br>
        <input type="submit" value="Submit">
    </form>
    <script src="main.js"></script>
</body>

</html>

6.2 TypeScript 表单验证逻辑

const form = document.getElementById('registrationForm');
if (form) {
    form.addEventListener('submit', (e) => {
        const emailInput = document.getElementById('email') as HTMLInputElement;
        const dateInput = document.getElementById('date') as HTMLInputElement;
        const phoneInput = document.getElementById('phone') as HTMLInputElement;

        const email = emailInput.value;
        const date = dateInput.value;
        const phone = phoneInput.value;

        if (!validateEmail(email)) {
            alert('Invalid email address');
            e.preventDefault();
        }
        if (!validateDate(date)) {
            alert('Invalid date format');
            e.preventDefault();
        }
        if (!validatePhone(phone)) {
            alert('Invalid phone number');
            e.preventDefault();
        }
    });
}

在上述代码中,当用户提交表单时,我们获取各个输入字段的值,并使用之前定义的类型安全验证函数进行验证。如果验证不通过,弹出提示信息并阻止表单提交。

7. 提高验证器的可复用性

7.1 创建验证器库

为了提高验证器的可复用性,我们可以将所有的验证函数封装成一个库。首先,创建一个 validators.ts 文件。

function validate<T>(regex: RegExp, value: T): boolean {
    if (typeof value === "string") {
        return regex.test(value);
    }
    return false;
}

const emailRegex = /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/;
export function validateEmail(email: string): boolean {
    return validate(emailRegex, email);
}

const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
export function validateDate(date: string): boolean {
    return validate(dateRegex, date);
}

const phoneRegex = /^\d{3}-\d{8}$/;
export function validatePhone(phone: string): boolean {
    return validate(phoneRegex, phone);
}

然后,在其他项目文件中,我们可以通过导入这个库来使用验证函数。

import { validateEmail, validateDate, validatePhone } from './validators';

// 使用验证函数
let email = "test@example.com";
let isValidEmail = validateEmail(email);

let date = "2023 - 10 - 01";
let isValidDate = validateDate(date);

let phone = "123 - 45678901";
let isValidPhone = validatePhone(phone);

7.2 基于配置的验证

我们还可以进一步实现基于配置的验证,使验证逻辑更加灵活。例如,我们可以定义一个验证规则的配置对象。

interface ValidationConfig {
    [key: string]: {
        regex: RegExp;
        errorMessage: string;
    };
}

const validationConfigs: ValidationConfig = {
    email: {
        regex: /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/,
        errorMessage: 'Invalid email address'
    },
    date: {
        regex: /^\d{4}-\d{2}-\d{2}$/,
        errorMessage: 'Invalid date format'
    },
    phone: {
        regex: /^\d{3}-\d{8}$/,
        errorMessage: 'Invalid phone number'
    }
};

function validateWithConfig(value: string, configKey: keyof ValidationConfig): { isValid: boolean; errorMessage: string } {
    const config = validationConfigs[configKey];
    if (!config) {
        return { isValid: false, errorMessage: 'Unknown validation type' };
    }
    const isValid = config.regex.test(value);
    return { isValid, errorMessage: config.errorMessage };
}

这样,在使用时可以通过配置键来选择不同的验证规则。

let email = "test@example.com";
let emailValidationResult = validateWithConfig(email, 'email');

let date = "2023 - 10 - 01";
let dateValidationResult = validateWithConfig(date, 'date');

let phone = "123 - 45678901";
let phoneValidationResult = validateWithConfig(phone, 'phone');

8. 处理国际化和本地化的验证

在全球化的应用中,不同地区可能有不同的日期、电话号码等格式。例如,日期格式在一些国家是“DD - MM - YYYY”,而在另一些国家是“MM/DD/YYYY”。

8.1 日期格式的国际化处理

我们可以根据用户的区域设置来选择不同的日期正则表达式。假设我们使用 Intl.DateTimeFormat 来获取用户的区域设置信息。

function getDateRegexByLocale(locale: string): RegExp {
    if (locale.startsWith('en - US')) {
        return /^\d{2}\/\d{2}\/\d{4}$/;
    } else if (locale.startsWith('en - GB')) {
        return /^\d{2}-\d{2}-\d{4}$/;
    }
    return /^\d{4}-\d{2}-\d{2}$/;
}

function validateDateByLocale(date: string, locale: string): boolean {
    const regex = getDateRegexByLocale(locale);
    return validate(regex, date);
}

8.2 电话号码格式的本地化处理

电话号码格式也因地区而异。我们可以维护一个地区代码和电话号码格式正则表达式的映射。

const phoneRegexByRegion: { [region: string]: RegExp } = {
    'US': /^\d{3}-\d{3}-\d{4}$/,
    'UK': /^0\d{10}$/,
    'CN': /^1[3 - 9]\d{9}$/
};

function validatePhoneByRegion(phone: string, region: string): boolean {
    const regex = phoneRegexByRegion[region];
    if (!regex) {
        return false;
    }
    return validate(regex, phone);
}

9. 性能优化与正则表达式缓存

9.1 正则表达式缓存

在频繁使用正则表达式进行验证时,每次创建正则表达式对象会带来一定的性能开销。我们可以通过缓存正则表达式对象来提高性能。

const regexCache: { [key: string]: RegExp } = {};

function getRegex(key: string, regexStr: string): RegExp {
    if (!regexCache[key]) {
        regexCache[key] = new RegExp(regexStr);
    }
    return regexCache[key];
}

function validateWithCachedRegex<T>(key: string, regexStr: string, value: T): boolean {
    const regex = getRegex(key, regexStr);
    if (typeof value === "string") {
        return regex.test(value);
    }
    return false;
}

9.2 性能测试

为了验证缓存正则表达式的性能提升,我们可以进行简单的性能测试。

const start = Date.now();
for (let i = 0; i < 10000; i++) {
    validateWithCachedRegex('email', '^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$', 'test@example.com');
}
const end = Date.now();
console.log(`Cached regex validation took ${end - start} ms`);

const start2 = Date.now();
for (let i = 0; i < 10000; i++) {
    validate(new RegExp('^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$'), 'test@example.com');
}
const end2 = Date.now();
console.log(`Non - cached regex validation took ${end2 - start2} ms`);

通过上述测试,我们可以明显看到缓存正则表达式在多次验证时的性能优势。

10. 与其他验证方式的结合

10.1 结合 JSONSchemaValidator

在处理 JSON 数据时,除了对单个字符串字段进行正则表达式验证,还可以结合 JSONSchemaValidator 进行更全面的验证。例如,假设我们有一个包含邮箱和日期字段的 JSON 数据结构。

import JSONSchemaValidator from 'jsonschema';

const userSchema = {
    type: 'object',
    properties: {
        email: { type:'string', pattern: '^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$' },
        dateOfBirth: { type:'string', pattern: '^\d{4}-\d{2}-\d{2}$' }
    },
    required: ['email', 'dateOfBirth']
};

function validateUser(user: any): boolean {
    const result = JSONSchemaValidator.validate(user, userSchema);
    return result.valid;
}

10.2 结合自定义验证函数

我们还可以在 JSONSchemaValidator 的基础上结合自定义的正则表达式验证函数。例如,如果我们希望在 JSONSchemaValidator 验证邮箱时使用我们之前定义的 validateEmail 函数。

const userSchema2 = {
    type: 'object',
    properties: {
        email: { type:'string' },
        dateOfBirth: { type:'string', pattern: '^\d{4}-\d{2}-\d{2}$' }
    },
    required: ['email', 'dateOfBirth']
};

function validateUser2(user: any): boolean {
    const result = JSONSchemaValidator.validate(user, userSchema2);
    if (!result.valid) {
        return false;
    }
    return validateEmail(user.email);
}

通过这种方式,我们可以充分利用不同验证方式的优势,构建更强大、更灵活的验证体系。

11. 错误处理与日志记录

11.1 验证失败时的错误处理

在验证过程中,当验证失败时,我们可以抛出特定的错误类型,以便调用者能够更好地处理。

class ValidationError extends Error {
    constructor(message: string) {
        super(message);
        this.name = 'ValidationError';
    }
}

function validateEmailWithError(email: string): void {
    if (!validateEmail(email)) {
        throw new ValidationError('Invalid email address');
    }
}

11.2 日志记录

在验证过程中,记录验证相关的日志有助于调试和监控。我们可以使用 console.log 或者专业的日志库,如 winston

import winston from 'winston';

const logger = winston.createLogger({
    level: 'info',
    format: winston.format.json(),
    transports: [
        new winston.transport.Console()
    ]
});

function validateDateWithLogging(date: string): boolean {
    const isValid = validateDate(date);
    if (isValid) {
        logger.info('Date validation passed');
    } else {
        logger.error('Date validation failed');
    }
    return isValid;
}

通过良好的错误处理和日志记录,我们可以提高验证系统的稳定性和可维护性。

12. 安全性考虑

12.1 防止正则表达式注入

在构建正则表达式时,如果使用用户输入来动态生成正则表达式,可能会面临正则表达式注入的风险。例如,假设我们有一个搜索功能,用户可以输入搜索模式。

// 不安全的做法
function searchUnsafe(input: string, text: string): boolean {
    const regex = new RegExp(input);
    return regex.test(text);
}

恶意用户可以输入特殊的正则表达式字符,从而改变正则表达式的行为。为了防止这种情况,我们应该对用户输入进行严格的过滤和验证。

function sanitizeInput(input: string): string {
    return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function searchSafe(input: string, text: string): boolean {
    const sanitizedInput = sanitizeInput(input);
    const regex = new RegExp(sanitizedInput);
    return regex.test(text);
}

12.2 敏感信息验证

在验证包含敏感信息(如密码)时,要注意不要在日志或错误信息中泄露敏感内容。例如,在密码验证失败时,错误信息应该是通用的,而不是包含密码本身。

function validatePassword(password: string): boolean {
    // 简单的密码强度正则表达式
    const passwordRegex = /^(?=.*[a - z])(?=.*[A - Z])(?=.*\d)[a-zA - Z\d]{8,}$/;
    return validate(passwordRegex, password);
}

try {
    validatePassword('WeakPassword123');
} catch (error) {
    if (error instanceof ValidationError) {
        console.log('Password validation failed. Please check password strength.');
    }
}

通过这些安全性考虑,我们可以确保验证过程的安全性,保护用户数据和系统安全。