TypeScript 高级类型:利用模板字面量与类型守卫处理日期字符串

作者:拾光

时间:2026-10-25

浏览:197

本文介绍在 TypeScript 中通过模板字面量类型和类型谓词处理自定义日期字符串的方法。利用 TypeScript 4.1 引入的模板字面量特性定义格式,结合用户自定义类型守卫进行运行时验证,并引入名义类型(Branded Types)确保类型安全,从而提升开发体验并减少潜在错误。

文章目录

引言:

在最近的项目开发中,我需要处理多种自定义的日期字符串格式,例如YYYY-MM-DD与YYYYMMDD。由于这些日期以字符串变量形式存在,TypeScript默认将其推断为string类型。尽管这在技术层面是正确的,但在实际工程中使用如此宽泛的类型定义会显著增加处理这些日期字符串的难度。例如,let dog = 'alfie'同样会被推断为string类型。

本文将展示我的解决方案,旨在通过优化这些日期字符串的输入处理来提升开发者体验并降低潜在错误率。

在开始编码前,简要回顾实现目标所需的关键typescript特性,包括模板字面量类型以及通过类型谓词进行范围缩小。

一、模板字面量类型

该特性于typescript4.1版本引入,其语法与JavaScript的模板字符串一致,但用于类型定义。模板字面量类型解析为基于给定模板的所有可能字符串组合的联合类型。这一概念可能略显抽象,

具体代码示例如下:

type Person = 'Jeff' | 'Maria'
type Greeting = `hi ${Person}!` // Template literal type
const validGreeting: Greeting = `hi Jeff!` // 
// note that the type of `validGreeting` is the union `"hi Jeff!" | "hi Maria!`
const invalidGreeting: Greeting = `bye Jeff!` // 
// Type '"bye Jeff!"' is not assignable to type '"hi Jeff!" | "hi Maria!"

模板字面量类型功能强大,支持对类型执行通用操作,例如转换为大写字母。

type Person = 'Jeff' | 'Maria'
type Greeting = `hi ${Person}!`
type LoudGreeting = Uppercase<Greeting> // Capitalization of template literal type
const validGreeting: LoudGreeting = `HI JEFF!` // 
const invalidGreeting: LoudGreeting = `hi jeff!` // 
// Type '"hi Jeff!"' is not assignable to type '"HI JEFF!" | "HI MARIA!"

二、类型谓词缩小范围

TypeScript在类型范围缩小方面表现优异,参考以下示例:

let age: string | number = getAge();
// `age` is of type `string` | `number`
if (typeof age === 'number') {
  // `age` is narrowed to type `number`
} else {
  // `age` is narrowed to type `string`
}

这意味着在处理自定义类型时,指导typescript编译器如何进行类型缩小至关重要。例如,在运行时验证后缩小至特定类型时,类型谓词窄化(或称用户定义的类型守卫)便发挥了作用。

在以下示例中,isDog类型守卫通过检查类型属性协助缩小animal变量的类型:

type Dog = { type: 'dog' };
type Horse = { type: 'horse' };
//  custom type guard, `pet is Dog` is the type predicate
function isDog(pet: Dog | Horse): pet is Dog {
  return pet.type === 'dog';
}
let animal: Dog | Horse = getAnimal();
// `animal` is of type `Dog` | `Horse`
if (isDog(animal)) {
  // `animal` is narrowed to type `Dog`
} else {
  // `animal` is narrowed to type `Horse`
}

三、定义日期字符串

为保持简洁,此示例仅展示YYYYMMDD格式日期字符串的代码。

首先,需定义模板字面量类型以表示所有类似日期字符串的联合类型。

type oneToNine = 1|2|3|4|5|6|7|8|9
type zeroToNine = 0|1|2|3|4|5|6|7|8|9
/**
 * Years
 */
type YYYY = `19${zeroToNine}${zeroToNine}` | `20${zeroToNine}${zeroToNine}`
/**
 * Months
 */
type MM = `0${oneToNine}` | `1${0|1|2}`
/**
 * Days
 */
type DD = `${0}${oneToNine}` | `${1|2}${zeroToNine}` | `3${0|1}`
/**
 * YYYYMMDD
 */
type RawDateString = `${YYYY}${MM}${DD}`;
const date: RawDateString = '19990223' // 
const dateInvalid: RawDateString = '19990231' //31st of February is not a valid date, but the template literal doesnt know!
const dateWrong: RawDateString = '19990299'//  Type error, 99 is not a valid day

由上述示例可知,模板字面量类型有助于指定日期字符串格式,但未对日期有效性进行实际验证。因此,编译器会将19990231标记为有效日期,即便其逻辑不正确,仅因其符合模板类型即可。

此外,检查上述变量如date、dateInvalid、dateWrong时,你会发现编辑器显示这些模板字面的所有有效字符联合。虽然有用,但我更倾向于设置名义类型,使有效日期字符串的类型为DateString,而非"19000101" | "19000102" | "19000103" | ...。在添加用户定义的类型保护时,名义类型同样具有价值。

type Brand<K, T> = K & { __brand: T };
type DateString = Brand<RawDateString, 'DateString'>;
const aDate: DateString = '19990101'; // 
// Type 'string' is not assignable to type 'DateString'

为确保我们的DateString类型也代表有效日期,我们将设置一个用户定义的类型保护来验证日期并缩小类型。

/**
 * Use `moment`, `luxon` or other date library
 */
const isValidDate = (str: string): boolean => {
  // ...
};
//User-defined type guard
function isValidDateString(str: string): str is DateString {
  return str.match(/^\d{4}\d{2}\d{2}$/) !== null && isValidDate(str);
}

接下来,观察几个示例中的日期字符串类型。在以下代码片段中,用户定义的类型保护被应用于类型缩小,允许TypeScript编译器将类型细化为比声明更具体的类型。随后,在工厂函数中应用类型保护,以从非标准化输入字符串创建有效日期字符串。

/**
 *   Usage in type narrowing
 */
// valid string format, valid date
const date: string = '19990223';
if (isValidDateString(date)) {
  // evaluates to true, `date` is narrowed to type `DateString` 
}
//  valid string format, invalid date (February doenst have 31 days)
const dateWrong: string = '19990231';
if (isValidDateString(dateWrong)) {
  // evaluates to false, `dateWrong` is not a valid date, even if its shape is YYYYMMDD 
}
/**
 *   Usage in factory function
 */
function toDateString(str: RawDateString): DateString {
  if (isValidDateString(str)) return str;
  throw new Error(`Invalid date string: ${str}`);
}
//  valid string format, valid date
const date1 = toDateString('19990211');
// `date1`, is of type `DateString`
//  invalid string format
const date2 = toDateString('asdf');
//  Type error: Argument of type '"asdf"' is not assignable to parameter of type '"19000101" | ...
//  valid string format, invalid date (February doenst have 31 days)
const date3 = toDateString('19990231');
//  Throws Error: Invalid date string: 19990231

总结:

希望本文能展示TypeScript在输入自定义字符串方面的能力。请注意,该方法同样适用于其他自定义字符串,如自定义user-ids、user-xxxx,以及其他日期字符串,例如YYYY-MM-DD。

结合用户定义的类型防护、模板字面字符串和名义类型,可能性无限。

至此,关于如何在TypeScript中处理日期字符串的文章介绍完毕,更多相关TypeScript处理字符串内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章