为什么 TypeScript 中的「Date」是一个陷阱?
\n想象一下:您正在编写一个应用程序,一切运行完美。但有一天,一个错误报告来了:「东京的用户看到的日期早了一天」。您打开代码,看到了经典场景:let date: Date = "2024-12-01"。或者更糟的是——any。熟悉吗?
Stack Overflow 上 「How do I express a date type in TypeScript?」 这个问题获得了数万次浏览。这并非没有原因。答案看似简单:使用 Date。但在实践中,情况要复杂得多。TypeScript 是 JavaScript 的超集,而 JS 中的 Date 是一个行为……独特的对象。
在本文中,我们将讨论的不仅仅是语法,而是正确的日期类型化,它将拯救您的项目免于错误,提高性能,并使代码对团队来说易于理解。您将学习如何在 TypeScript 中处理真实日期,而无需浪费时间调试。
\n\n1. 基础类型化:Date、string 还是 number?
\n最明显的选择是内置类型 Date:
let birthday: Date = new Date('1990-05-15');\n有效吗?是的。但有一个问题:TypeScript 不检查日期是否有效。您可以编写 new Date('abc')——编译时不会报错,但在运行时您会得到 Invalid Date。而尝试调用 .getTime() 将返回 NaN。
更严格的选项是使用ISO 8601 格式的字符串:
\ntype ISODateString = string;\nlet eventDate: ISODateString = '2024-12-01T10:00:00Z';\n优点:类型简单,易于序列化为 JSON。缺点:没有任何验证——您可以赋值 'hello',而 TS 不会报错。
第三种选项是时间戳 (number):
\ntype Timestamp = number;\nlet ms: Timestamp = Date.now();\n非常适合数学运算,但对人类来说不可读。同样——任何数字都能通过检查。
\n\n基础类型总结
\n如果您需要快速而粗糙的类型化——请使用 Date。如果您与 API 交互——请使用 string。如果只进行计算——请使用 number。但对于严肃的项目来说,这还不够。让我们深入探讨。
2. 自定义类型和品牌类型化 (branded types)
\n为了让 TypeScript 不将日期字符串与普通字符串混淆,可以使用品牌类型。这就像贴了一个标签:「我是日期,已验证!」:
\ntype DateString = string & { __brand: 'DateString' };\n\nfunction toDateString(date: Date): DateString {\n return date.toISOString() as DateString;\n}\n\nfunction processDate(d: DateString) {\n // 这里 d 肯定是 ISO 字符串\n}\n\nconst iso = toDateString(new Date());\nprocessDate(iso); // 正常\n// processDate('hello'); // ❌ 错误!\n这增加了编译时的安全性。代价是代码稍多一些。但对于大型项目(尤其是如果您有微服务或 REST API),这是黄金标准。
\n\n3. 用于 TypeScript 日期处理的库
\n原生的 Date 是邪恶的。它是可变的,解析字符串不一致,时区更是独立的痛点。因此,在实际项目中会使用库。以下是三个最受欢迎的:
3.1. date-fns(轻量且模块化)
\nimport { format, parseISO } from 'date-fns';\n\nconst date: Date = parseISO('2024-12-01T10:00:00Z');\nconst formatted: string = format(date, 'dd.MM.yyyy');\n优点:支持 tree-shaking,不可变(返回新日期),类型化出色。
\n\n3.2. Day.js(极简主义)
\nimport dayjs, { Dayjs } from 'dayjs';\n\nconst now: Dayjs = dayjs();\nconst formatted: string = now.format('DD.MM.YYYY');\nDay.js 就像 Moment.js,但没有臃肿的包体积。Dayjs 类型是一个包装器,可以轻松地在函数之间传递。