t3code 工程中的 Effect DateTime 实践:以可测试、可移植的方式处理日期与时间
【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code
导读
在 t3code 这个横跨桌面(Electron)、移动端(React Native)与服务端(Node)的大型 TypeScript 工程中,大量 Effect 程序需要处理「当前时间」「日期字符串解析」「时区转换」「ISO 格式化」等需求。本指南基于仓库内.repos/effect-smol的官方 AI 文档与源码,系统讲解为何应使用DateTime模块替代原生Date/Date.now,并给出可复制、可运行的创建、格式化、时区转换与日历运算完整示例,帮助你写出可测试、无副作用、时区正确的日期时间代码。
为什么用DateTime而不是Date与Date.now
原文核心结论:处理日期和时间时,请使用
DateTime模块,而不是Date和Date.now。
原生Date的问题在于:当前时间获取(Date.now/new Date())是隐式的、不可注入的全局副作用,它直接绑定宿主时钟;字符串解析行为松散且时区语义混乱;格式化输出依赖toISOString/toLocaleString等 API,在不同运行时(Node、浏览器、React Native Hermes)表现不一致,难以写出稳定断言。
DateTime模块把「时间」建模为一等公民的 Effect 值,主要解决四类需求(见 index.md):
- 可测试的当前时间:通过 Effect 的
Clock服务获取当前时间,测试中可用TestClock精确控制时间流逝; - 安全的解析:
DateTime.make等解析 API 返回Option,非法输入不会抛异常而是得到None; - 稳定的 ISO 格式化:
formatIso系列提供确定性的、跨运行时一致的输出; - 时区转换与日历运算:支持 IANA 时区名(如
Pacific/Auckland)与不可变的日历加减。
从源码看,DateTime.now的类型为Effect.Effect<Utc>(DateTime.ts),DateTime.nowInCurrentZone为Effect.Effect<Zoned, never, CurrentTimeZone>(DateTime.ts),也就是说时间获取被建模成对Clock/CurrentTimeZone服务的依赖,天然支持依赖注入与测试替身。
创建与格式化 DateTime 值
以下示例完整摘录自.repos/effect-smol/ai-docs/src/07_datetime/10_creating-and-formatting.ts,演示了本模块最核心的四个操作。
import { DateTime, Effect, Option } from "effect" Effect.gen(function*() { // 1. 从 Effect 的 Clock 服务获取当前时间(UTC) // 使用 Clock 服务意味着测试可以用 TestClock 模块控制时间 const now = yield* DateTime.now // 2. 用 DateTime.make 安全解析日期输入(如用户输入的字符串或 epoch 时间戳) // 返回 Option,取决于输入是否合法 const parsedOption: Option.Option<DateTime.Utc> = DateTime.make("2024-06-15T14:30:00.000Z") // 3. 用 Option API 解包 Option.getOrUndefined(parsedOption) // 4. 日历/日期时间运算返回一个新的 DateTime 值,原值不可变 const endsAt = now.pipe(DateTime.add({ hours: 2 })) // 5. format* 系列函数将 DateTime 转换为不同格式 yield* Effect.log("ISO string:", DateTime.formatIso(endsAt)) })要点拆解
DateTime.now:返回Effect<Utc>,不是同步的Date对象。它从Clock服务读取时间,因此Effect.gen中必须yield*。测试时可用TestClock把时间"拨"到任意时刻,再断言日志或持久化内容。DateTime.make:源码签名export const make: <A extends DateTime.Input>(input: A) => Option.Option<DateTime.PreserveZone<A>>(DateTime.ts),接受字符串、epoch 毫秒/秒数或日期字段对象等输入,返回Option——解析失败得到None,而非抛出异常。对于"用户输入的日期字符串"这类不可信输入,这是推荐入口。- 不可变运算:
DateTime.add({ hours: 2 })返回新的DateTime值,原值不被修改。运算字段除hours外还支持minutes、seconds、days、months、years等日历单位,这是Date#setHours这类原地变更 API 无法安全提供的语义。add的完整重载定义见 DateTime.ts。 - 稳定格式化:
DateTime.formatIso(DateTime.ts)输出如2024-06-15T16:30:00.000Z的标准 UTC ISO 字符串,适合写入 API payload 或数据库;面向用户展示时可配合formatIsoDate、formatIsoTime等变体。
处理时区:附加 IANA 时区、渲染带时区 ISO 字符串
跨时区是日期时间代码最容易出错的场景。以下完整示例摘录自.repos/effect-smol/ai-docs/src/07_datetime/20_time-zones.ts,展示了三种把时区附加到 DateTime 的方式,以及CurrentTimeZone服务的使用。
import { NodeRuntime } from "@effect/platform-node" import { DateTime, Effect, Option } from "effect" Effect.gen(function*() { // 从 Clock 服务获取当前时间 const now = yield* DateTime.now // 方式一:附加已知有效的 IANA 时区(Unsafe,节省一次 Option 判断) const nowInAuckland = now.pipe( DateTime.setZoneNamedUnsafe("Pacific/Auckland") ) yield* Effect.log("Now in Auckland:", nowInAuckland) // 方式二:附加未知是否有效的 IANA 时区(返回 Option) const nowInSydneyOption: Option.Option<DateTime.Zoned> = now.pipe( DateTime.setZoneNamed("Australia/Sydney") ) yield* Effect.log("Now in Sydney:", Option.getOrUndefined(nowInSydneyOption)) // 方式三:直接生成位于 CurrentTimeZone 服务的 DateTime.Zoned const nowInNewYork = yield* DateTime.nowInCurrentZone yield* Effect.log("Now in New York:", nowInNewYork) // 已知某个日期字符串属于特定 IANA 时区时,转换为 Zoned 以保证 instant 正确 const dateInAuckland: DateTime.Zoned = DateTime.makeZonedUnsafe("2026-06-05", { timeZone: "Pacific/Auckland", // adjustForTimeZone 会将输入调整为给定时区的时间; // 否则输入会被当作 UTC 处理 adjustForTimeZone: true }) yield* Effect.log("Date in Auckland:", dateInAuckland) }).pipe( // 为 CurrentTimeZone 服务提供实现:这里固定为纽约时区 Effect.provide(DateTime.layerCurrentZoneNamed("America/New_York")), NodeRuntime.runMain )三种附加时区方式的取舍
| API | 签名语义 | 适用场景 |
|---|---|---|
setZoneNamedUnsafe(zone) | 已知时区名一定有效时使用,直接返回DateTime.Zoned(DateTime.ts) | 时区名来自受控配置、白名单 |
setZoneNamed(zone) | 返回Option.Option<DateTime.Zoned>,无效时区得到None | 时区名来自用户输入或不可信配置 |
nowInCurrentZone | 从CurrentTimeZone服务读取当前时区并生成 Zoned | 需要跟随"工作区/用户时区"的代码 |
CurrentTimeZone服务:让"当前时区"可注入
DateTime.nowInCurrentZone的效果类型是Effect<Zoned, never, CurrentTimeZone>,即它依赖CurrentTimeZone服务。示例代码通过Effect.provide(DateTime.layerCurrentZoneNamed("America/New_York"))注入实现(layerCurrentZoneNamed的 Layer 定义见 DateTime.ts)。
这种设计的工程价值在于:业务代码不再隐式依赖操作系统时区。在 t3code 这类需要跟随用户/工作区时区的场景(如日志时间戳、会话记录、截止时间展示),可以将"当前时区"作为服务注入,测试时注入固定时区即可获得确定性输出,而不必修改全局process.env.TZ。
adjustForTimeZone的陷阱
DateTime.makeZonedUnsafe("2026-06-05", { timeZone: "Pacific/Auckland", adjustForTimeZone: true }):
- 当
adjustForTimeZone: true时,2026-06-05被解释为奥克兰当地时间的 6 月 5 日零点,其底层 UTC instant 会根据奥克兰与 UTC 的偏移换算; - 当省略或为
false时,输入先被当作 UTC,再套用时区外壳。
makeZonedUnsafe的完整签名(含timeZoneId与adjustForTimeZone选项)见 DateTime.ts。选择错误会导致实际 instant 偏移数小时,这一点在跨时区业务中必须格外注意。
结合源码看DateTime的可靠性与可测试性
- Option 驱动解析:
make、setZoneNamed等 API 统一返回Option,配合Option.map/Option.getOrUndefined/Option.getOrThrow组合使用,保证非法日期在类型层面就被约束,不会以运行时异常的形式泄漏。 - Clock 抽象:所有"当前时间"都经过
Clock服务。测试中注入TestClock后,可以自由推进时间以模拟"两小时后""明天零点"等边界,无需真实等待,这也是DateTime.now与Date.now在测试体验上的根本差异。 - Zoned 类型携带时区:
DateTime.Zoned的格式化输出形如2024-06-15T15:30:00.000+01:00[Europe/London](见 DateTime.ts 的 JSDoc 示例),既包含偏移量又包含 IANA 时区名,比裸的+01:00偏移更精确——偏移会因夏令时而变化,IANA 时区名则是稳定的语义标识。 - 跨运行时一致:
formatIso系列输出纯字符串、无 locale 依赖,在 Node、浏览器与 React Native(Hermes)等 t3code 各端运行时上结果一致,便于写跨端快照测试。
适用前提与注意事项
- 本指南对应的
DateTime模块来自仓库内.repos/effect-smol/packages/effect/src/DateTime.ts,属于 Effect V4(release candidate)形态;若项目中锁定的是 Effect V3,API 名称与签名可能略有差异,请以项目实际依赖版本为准。 - 使用
DateTime要求代码运行在 Effect 上下文(Effect.gen+ run 函数,如NodeRuntime.runMain)中;纯同步场景下DateTime.now这类 Effect 值需要被执行后才有实际时间。 - 面向用户展示的本地化格式(如"2026 年 9 月 14 日")仍需要结合 locale 处理,
DateTime提供的是稳定的机器可读格式与正确的 instant 语义,二者分工不同。
延伸阅读
- 指南原文:DateTime 模块使用说明
- 创建与格式化完整示例:10_creating-and-formatting.ts
- 时区处理完整示例:20_time-zones.ts
- 模块源码:packages/effect/src/DateTime.ts
- 测试用例:packages/effect/test/DateTime.test.ts(含 DST 切换、闰年等边界场景验证)
- Effect 工程说明与依赖要求:.repos/effect-smol/README.md(需要 TypeScript 5.9+ 且开启
strict)
【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考