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

TypeScript模板字面量类型实现智能字符串校验

2022-03-232.1k 阅读

TypeScript 模板字面量类型简介

在 TypeScript 中,模板字面量类型是一项强大的功能,它允许我们基于已有的类型动态地生成新的字符串类型。模板字面量类型的语法与 JavaScript 中的模板字符串类似,使用反引号(`)包裹,并且可以在其中嵌入类型表达式。

例如,假设我们有两个类型 FirstSecond,我们可以这样定义一个模板字面量类型:

type First = 'hello';
type Second = 'world';
type Combined = `${First} ${Second}`;
// Combined 类型现在是 'hello world'

这里,Combined 类型就是通过模板字面量将 FirstSecond 组合在一起形成的新字符串类型。

模板字面量类型不仅可以用于简单的字符串拼接,还能结合类型变量和条件类型等其他 TypeScript 特性,实现非常复杂的类型操作。

基本的模板字面量类型拼接

我们先从最基础的字符串拼接场景来看模板字面量类型的应用。在实际开发中,我们经常需要处理不同部分组合而成的字符串。比如,在构建 URL 时,可能需要将协议、主机名、路径等部分拼接起来。

type Protocol = 'http' | 'https';
type Host = 'example.com';
type Path = '/home' | '/about';

type Url = `${Protocol}://${Host}${Path}`;
// Url 类型是 'http://example.com/home' | 'http://example.com/about' | 'https://example.com/home' | 'https://example.com/about'

在这个例子中,Url 类型通过模板字面量将 ProtocolHostPath 组合在一起,生成了所有可能的 URL 字符串类型。这样,在编写处理 URL 的代码时,TypeScript 就能准确地对传入的 URL 进行类型检查,避免错误的 URL 格式。

结合类型变量的模板字面量类型

模板字面量类型与类型变量结合使用时,可以实现更灵活的类型生成。假设我们有一个函数,它接受一个字符串前缀和一个数字,然后返回一个带有前缀的编号字符串。我们可以使用类型变量来定义这个函数的类型。

function prefixNumber(prefix: string, num: number): `${string}-${number}` {
    return `${prefix}-${num}`;
}

let result = prefixNumber('item', 1);
// result 的类型是 'item-1'

这里,函数 prefixNumber 的返回类型使用了模板字面量类型 ${string}-${number},其中 stringnumber 就是类型变量。这种方式使得函数返回值的类型能够根据传入的参数动态生成。

智能字符串校验的需求背景

在许多编程场景中,我们需要对输入的字符串进行特定格式的校验。比如,校验邮箱格式、日期格式、身份证号码格式等。传统的方式通常是使用正则表达式在运行时进行校验。虽然正则表达式功能强大,但它存在一些缺点。首先,正则表达式的语法复杂,编写和维护成本较高。其次,运行时的正则校验会带来一定的性能开销。

而 TypeScript 的模板字面量类型提供了一种在编译时进行字符串校验的方式,即智能字符串校验。通过在类型层面定义字符串的格式规则,TypeScript 编译器可以在编译阶段就发现不符合格式要求的字符串,从而提前避免运行时错误,提高代码的健壮性和可靠性。

基于模板字面量类型的邮箱格式校验

邮箱格式是一种常见的需要校验的字符串格式。一个合法的邮箱地址通常由用户名、@符号和域名组成。我们可以使用模板字面量类型来定义邮箱地址的类型。

type EmailPart = string & { __emailPartBrand: never };
type DomainPart = string & { __domainPartBrand: never };

type Email = `${EmailPart}@${DomainPart}`;

function sendEmail(to: Email, message: string) {
    // 发送邮件的逻辑
    console.log(`Sending email to ${to}: ${message}`);
}

let validEmail: Email = 'user@example.com';
sendEmail(validEmail, 'Hello!');

// 以下代码会报错,因为不符合 Email 类型
// let invalidEmail: Email = 'user.example.com';
// sendEmail(invalidEmail, 'Test');

在这个例子中,我们首先定义了 EmailPartDomainPart 类型,它们都是字符串类型,但通过品牌类型(__emailPartBrand__domainPartBrand)进行了区分,以确保不会与普通字符串类型混淆。然后,Email 类型通过模板字面量将 EmailPartDomainPart 组合在一起,定义了邮箱地址的格式。

当我们尝试将不符合邮箱格式的字符串赋值给 Email 类型的变量时,TypeScript 编译器会报错,从而实现了编译时的邮箱格式校验。

日期格式校验

日期格式也是经常需要校验的内容。假设我们希望校验的日期格式为 YYYY - MM - DD,我们可以这样实现:

type Year = `${number}`;
type Month = `0${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}` | `1${0 | 1 | 2}`;
type Day = `0${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}` | `${1 | 2}${0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}` | `3${0 | 1}`;

type DateFormat = `${Year}-${Month}-${Day}`;

function logDate(date: DateFormat) {
    console.log(`The date is ${date}`);
}

let validDate: DateFormat = '2023-05-15';
logDate(validDate);

// 以下代码会报错,因为不符合 DateFormat 类型
// let invalidDate: DateFormat = '2023-13-15';
// logDate(invalidDate);

这里,我们分别定义了 YearMonthDay 类型,通过模板字面量将它们组合成 DateFormat 类型。MonthDay 类型中使用了联合类型来限制合法的值范围,从而实现了对日期格式的校验。

身份证号码校验

身份证号码的格式较为复杂,以 18 位身份证号码为例,前 6 位是地址码,接下来 8 位是出生日期码,再接下来 3 位是顺序码,最后 1 位是校验码。虽然完整的身份证号码校验还涉及到校验码的计算等复杂逻辑,但我们可以通过模板字面量类型实现部分格式校验。

type AddressCode = `${number}`;
type BirthDateCode = `${number}`;
type SequenceCode = `${number}`;
type CheckCode = string & { __checkCodeBrand: never };

type IDCard = `${AddressCode}${BirthDateCode}${SequenceCode}${CheckCode}`;

function verifyIDCard(id: IDCard) {
    console.log(`Verifying ID card: ${id}`);
}

let validIDCard: IDCard = '11010519491001001X';
verifyIDCard(validIDCard);

// 以下代码会报错,因为长度不符合 IDCard 类型
// let invalidIDCard: IDCard = '11010519491001001';
// verifyIDCard(invalidIDCard);

在这个示例中,我们定义了不同部分的类型,并通过模板字面量组合成 IDCard 类型。虽然这里没有实现完整的校验逻辑,但通过类型定义可以确保身份证号码的基本格式正确。

模板字面量类型与条件类型结合实现更复杂校验

在一些情况下,我们需要根据不同的条件来生成不同的字符串类型。这时,模板字面量类型可以与条件类型结合使用。

假设我们有一个函数,它根据一个布尔值来生成不同格式的字符串。如果是 true,生成的字符串格式为 prefix - value;如果是 false,生成的字符串格式为 value - suffix

type Prefix = 'pre';
type Suffix ='suf';

type ConditionalString<T extends boolean> = T extends true
   ? `${Prefix}-${string}`
    : `${string}-${Suffix}`;

function generateString<T extends boolean>(flag: T, value: string): ConditionalString<T> {
    return flag? `${Prefix}-${value}` : `${value}-${Suffix}`;
}

let result1 = generateString(true, 'test');
// result1 的类型是 'pre - test'

let result2 = generateString(false, 'test');
// result2 的类型是 'test - suf'

在这个例子中,ConditionalString 类型使用条件类型来根据传入的布尔值 T 生成不同的模板字面量类型。函数 generateString 的返回类型也根据传入的 flag 的类型动态生成相应格式的字符串类型。

模板字面量类型在类型映射中的应用

类型映射是 TypeScript 中一种强大的类型操作,它允许我们基于现有的类型创建新的类型。模板字面量类型在类型映射中也能发挥重要作用。

假设我们有一个对象类型,我们希望为每个属性名添加前缀或后缀。

type OriginalObject = {
    name: string;
    age: number;
};

type PrefixedObject = {
    [K in keyof OriginalObject as `prefix_${K}`]: OriginalObject[K];
};

type SuffixedObject = {
    [K in keyof OriginalObject as `${K}_suffix`]: OriginalObject[K];
};

let prefixedObj: PrefixedObject = {
    prefix_name: 'John',
    prefix_age: 30
};

let suffixedObj: SuffixedObject = {
    name_suffix: 'John',
    age_suffix: 30
};

PrefixedObjectSuffixedObject 的定义中,我们使用类型映射和模板字面量类型,为 OriginalObject 的每个属性名添加了前缀或后缀,同时保留了属性值的类型。

模板字面量类型的局限性

虽然模板字面量类型在智能字符串校验等方面提供了强大的功能,但它也存在一些局限性。

首先,模板字面量类型主要适用于简单的字符串格式校验。对于非常复杂的字符串格式,如复杂的正则表达式所能表达的格式,模板字面量类型可能无法完全实现。例如,匹配嵌套括号的字符串,使用模板字面量类型很难精确描述其格式。

其次,模板字面量类型是在编译时进行检查的,它无法处理运行时动态生成的字符串。如果字符串的格式取决于运行时的用户输入或其他动态数据,模板字面量类型就无法提供有效的校验。

另外,模板字面量类型生成的类型可能会变得非常庞大和复杂,尤其是在涉及多个联合类型和嵌套的模板字面量时。这可能会导致编译时间变长,并且类型错误信息也可能变得难以理解。

与其他字符串校验方式的比较

与传统的正则表达式校验相比,模板字面量类型校验在编译时进行,能够提前发现错误,而正则表达式校验是在运行时进行,可能在程序运行到相关代码时才发现问题。模板字面量类型校验的代码更简洁,不需要编写复杂的正则表达式,易于理解和维护。但正则表达式在表达复杂格式方面更加强大,能够处理一些模板字面量类型无法处理的情况。

与第三方验证库(如 AJV 用于 JSON 数据验证)相比,模板字面量类型是 TypeScript 语言内置的功能,不需要引入额外的库,与 TypeScript 代码集成度更高。但第三方验证库通常功能更全面,支持更多类型的数据验证和复杂的验证规则,适用于更大型和复杂的应用场景。

实际项目中的应用场景

在实际项目中,模板字面量类型实现的智能字符串校验有很多应用场景。

在前端开发中,表单输入校验是一个常见的场景。例如,校验用户输入的手机号码、邮政编码等格式。通过模板字面量类型,可以在编译时就确保表单处理函数接收到的输入符合预期格式,减少运行时错误。

在后端开发中,API 接口的参数校验也可以使用模板字面量类型。比如,校验 API 路径参数、查询参数的格式。这有助于提高 API 的稳定性和安全性,避免因错误的参数格式导致的系统异常。

在配置文件处理中,如果配置文件中的某些字段有特定的字符串格式要求,使用模板字面量类型可以在读取配置文件时进行类型检查,确保配置的正确性。

最佳实践与建议

在使用模板字面量类型实现智能字符串校验时,有以下一些最佳实践和建议。

首先,尽量保持模板字面量类型的简洁性。避免过度复杂的嵌套和联合类型,以免导致类型难以理解和维护。如果确实需要处理复杂格式,可以考虑将其分解为多个简单的模板字面量类型组合。

其次,合理使用品牌类型。品牌类型可以有效地避免类型混淆,特别是在处理类似字符串类型但含义不同的情况时,如前面邮箱格式校验中的 EmailPartDomainPart

另外,结合其他 TypeScript 特性,如条件类型、类型映射等,可以发挥模板字面量类型的更大威力。但在使用这些组合特性时,要注意代码的可读性和可维护性。

最后,对于运行时动态生成的字符串,不能仅仅依赖模板字面量类型校验,还需要结合运行时的验证机制,如正则表达式校验或第三方验证库,以确保数据的完整性和正确性。

通过充分理解和运用模板字面量类型的特性,我们可以在 TypeScript 项目中实现高效、可靠的智能字符串校验,提升代码的质量和可维护性。在实际应用中,根据具体的需求和场景,灵活选择合适的字符串校验方式,能够更好地满足项目的要求。无论是前端还是后端开发,模板字面量类型都为我们提供了一种强大的工具,帮助我们在编译阶段就发现字符串格式相关的问题,减少潜在的运行时错误。同时,我们也要清楚其局限性,合理地与其他校验方式结合使用,以构建健壮的软件系统。在未来的 TypeScript 发展中,模板字面量类型可能会进一步完善和扩展,为开发者提供更多便利和功能,我们需要持续关注并学习新的特性和应用方法。