Effect Schema 日期时间与时区 Schema 全解析:DateTimeZoned、TimeZone、TimeZoneOffset 与 TimeZoneNamed 实战指南
【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect
在跨时区业务系统(如国际化 SaaS、日程调度、日志与审计、API 数据交换)中,日期时间的建模与序列化始终是绕不开的痛点:既要保留 IANA 命名的时区语义(如Europe/London),又要兼容简单的固定偏移(如+03:00),还要确保这些值能安全地穿越 JSON 边界。本篇文章基于 Effect 仓库中关于 Schema 新增DateTimeZoned、TimeZoneOffset、TimeZoneNamed、TimeZone四个 Schema 的变更记录(见 .changeset/pre/add-schema-datetime.md),结合 Schema.ts 与 DateTime.ts 的源码实现及 Schema.test.ts 中的测试用例,系统讲解这些 Schema 的用途、JSON 编解码行为、Arbitrary 生成规则与选型建议。读完本文,你将能熟练地为带时区的日期时间数据设计严谨的 Schema,并正确地在字符串、数字与DateTime值之间完成类型安全的转换。
变更背景:Schema 与 DateTime 模块的正式打通
该变更记录(changeset)声明:
Schema: add
DateTimeZoned,TimeZoneOffset,TimeZoneNamed, andTimeZoneschemas.
也就是说,Effect 的 Schema 模块(effect/Schema)正式引入了对DateTime模块核心类型的 Schema 支持。在此之前,Schema 只能以通用String/Number处理日期时间,无法区分 UTC、命名时区、偏移时区与带时区的完整时刻。本次新增的四个 Schema 与既有的DateTimeUtc系列一起,构成了完整的日期时间 Schema 家族,其源码统一位于 Schema.ts 的 "DateTime schemas" 区块。
从版本标注看:
DateTimeUtc、TimeZoneOffset、TimeZoneNamed、TimeZone、DateTimeZoned自3.10.0起提供(声明式 Schema,用于校验已存在的DateTime值);DateTimeUtcFromString、DateTimeUtcFromMillis、TimeZoneNamedFromString、TimeZoneFromString、DateTimeZonedFromString自4.0.0起提供(转换式 Schema,用于从原始输入解析)。
在深入四个 Schema 之前,先明确DateTime模块中时区(TimeZone)的两种底层类型,这是理解一切的基础。
前置知识:DateTime 模块中的两种时区类型
DateTime.TimeZone是带判别联合类型,包含两个分支(源码见 DateTime.ts):
DateTime.TimeZone.Named:使用 IANA 时区标识(如"Europe/London"、"Asia/Tokyo")。它承载完整的 DST(夏令时)规则,能根据具体时刻正确换算。创建方式为DateTime.zoneMakeNamed/DateTime.zoneMakeNamedUnsafe。DateTime.TimeZone.Offset:固定偏移量,内部以毫秒表示(如3 * 60 * 60 * 1000表示 UTC+3)。创建方式为DateTime.zoneMakeOffset,其offset属性即毫秒数,可经DateTime.zoneToString序列化为"+03:00"形式。
这两个分支通过DateTime.isTimeZoneOffset/DateTime.isTimeZoneNamed判别函数区分——这正是后续各 Schema 声明式declare校验的依据。
Schema 逐一详解:四个新增 Schema 的建模与编解码
Schema.TimeZoneOffset:毫秒偏移量 ↔ 数字
TimeZoneOffset是declare声明式 Schema,类型层面对应DateTime.TimeZone.Offset,用于校验已经构造好的偏移时区值。其默认 JSON 序列化行为(源码 Schema.ts):
- 编码:将
DateTime.TimeZone.Offset编码为数字(毫秒偏移量); - 解码:将数字通过
DateTime.zoneMakeOffset(n)还原为偏移时区值。
底层转换由timeZoneOffsetFromNumber完成(Schema.ts):decode调用DateTime.zoneMakeOffset(n),encode直接取tz.offset。toFormatter则使用DateTime.zoneToString输出人类可读的"+03:00"形式。
测试用例(Schema.test.ts)验证了DateTime.zoneMakeOffset(3 * 60 * 60 * 1000)可被正常解码与编码。
import * as DateTime from "effect/DateTime" import * as Schema from "effect/Schema" const schema = Schema.TimeZoneOffset // 解码:直接传入已构造的偏移时区值 Schema.decodeSync(schema)(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000)) // 编码:得到毫秒偏移量数字 Schema.encodeSync(schema)(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000)) // => 10800000Schema.TimeZoneNamed:IANA 命名时区 ↔ 字符串
TimeZoneNamed同样为声明式 Schema,校验DateTime.TimeZone.Named值(Schema.ts)。默认 JSON 序列化行为:
- 编码:输出 IANA 时区标识字符串(如
"Europe/London"); - 解码:要求输入已是
DateTime.TimeZone.Named值。
其转换底层为timeZoneNamedFromString(Schema.ts):encode返回tz.id;decode调用DateTime.zoneMakeNamed(s),若无法识别则产生"a valid IANA time zone"的InvalidValue失败(对应测试 Schema.test.ts 中Europe/London的成功往返)。
值得注意的差异:TimeZoneNamed的默认 JSON 编解码并不接受任意字符串解析(它只校验已构造的 Named 值,字符串解析由后面的TimeZoneNamedFromString负责),但其Arbitrary 生成能力十分明确——从arbitraryNamedTimeZones常量("UTC"、"Europe/London"、"America/New_York"、"Asia/Tokyo"、"Australia/Sydney")中随机选取,再经DateTime.zoneMakeNamedUnsafe构造,见 Schema.ts 与 Schema.ts。
Schema.TimeZone:时区联合类型的"大统一"Schema
TimeZone同时覆盖 Named 与 Offset 两种分支,是对DateTime.TimeZone整个类型的声明式 Schema(Schema.ts)。默认 JSON 序列化行为:
- 编码:统一输出字符串——命名时区输出 IANA 标识(
"Europe/London"),偏移时区输出"+03:00"风格; - 解码:要求输入已是
DateTime.TimeZone值(Named 或 Offset 均可)。
底层转换timeZoneFromString(Schema.ts)的decode使用DateTime.zoneFromString,encode使用DateTime.zoneToString。测试(Schema.test.ts)验证了偏移时区与命名时区都能成功往返。
TimeZone的 Arbitrary 生成策略最有代表性(Schema.ts):它构造一个Union——dateTimeArbitraryInteger(-12h, +14h)(毫秒偏移数字)与Literals(arbitraryNamedTimeZones)(五个 IANA 标识)的联合,随后decode时根据值的类型(数字 →zoneMakeOffset,字符串 →zoneMakeNamedUnsafe)分别构造对应分支。
import * as Schema from "effect/Schema" const schema = Schema.TimeZone // 编码:命名时区 → IANA 字符串 Schema.encodeSync(schema)(DateTime.zoneMakeNamedUnsafe("Europe/London")) // => "Europe/London" // 编码:偏移时区 → "+03:00" 字符串 Schema.encodeSync(schema)(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000)) // => "+03:00"Schema.DateTimeZoned:保留时区语义的完整时刻
DateTimeZoned是对DateTime.Zoned(时刻 + 时区)的声明式 Schema,是最具实战价值的一个(Schema.ts)。它的默认 JSON 序列化行为定义了两套格式:
- 偏移时区:编码为带数字偏移的 ISO 日期时间,如
YYYY-MM-DDTHH:mm:ss.sss+HH:MM; - 命名时区:编码为 ISO 日期时间 + 方括号包裹的 IANA 标识,如
2024-01-01T00:00:00.000+00:00[Europe/London](即DateTime.formatIsoZoned的输出,见 DateTime.ts)。
底层转换dateTimeZonedFromString(Schema.ts)的decode使用DateTime.makeZonedFromString,encode使用DateTime.formatIsoZoned。注意DateTimeZoned本身只校验已构造的DateTime.Zoned值;对字符串的解析由 4.0.0 的DateTimeZonedFromString承担。
DateTimeZoned的 Arbitrary 生成逻辑(Schema.ts)非常精细:
- 时刻通过
dateTimeArbitraryBounds约束在±8640000000000000 ms并内缩14h(arbitraryMinimumZonedDateTimeTimestamp/arbitraryMaximumZonedDateTimeTimestamp),以容纳任意时区偏移后仍在合法日期范围内; - 时区复用
timeZoneArbitrarySchema()(偏移数字与 IANA 标识的 Union); - 最终以
{ epochMilliseconds, timeZone }结构经DateTime.makeZonedUnsafe构造出DateTime.Zoned。
同时DateTimeZoned还通过toEquivalence: () => DateTime.Equivalence提供了基于时刻的等价性判断(Schema.ts)。
import * as DateTime from "effect/DateTime" import * as Schema from "effect/Schema" const schema = Schema.DateTimeZoned const zoned = DateTime.makeZonedUnsafe("2021-01-01T00:00:00.000Z", { timeZone: "Europe/London" }) // 编码:命名时区 → ISO + [IANA] Schema.encodeSync(schema)(zoned) // => "2021-01-01T00:00:00.000+00:00[Europe/London]"4.0.0 起可用的 FromString 系列:从原始输入安全解析
上述四个声明式 Schema 只负责校验"已经是 DateTime 值"的数据;要从字符串/数字原始输入解析出DateTime值,需要 4.0.0 引入的转换式变体,它们均基于decodeTo实现:
| Schema | 输入 | 输出 | 底层解析函数 |
|---|---|---|---|
TimeZoneNamedFromString | string(IANA 标识) | DateTime.TimeZone.Named | DateTime.zoneMakeNamed |
TimeZoneFromString | string(IANA 标识或+03:00) | DateTime.TimeZone | DateTime.zoneFromString |
DateTimeZonedFromString | string(...Z[Europe/London]格式) | DateTime.Zoned | DateTime.makeZonedFromString |
DateTimeUtcFromString | string(可被DateTime.make接受的日期时间串) | DateTime.Utc | DateTime.make+toUtc |
DateTimeUtcFromMillis | number(epoch 毫秒) | DateTime.Utc | DateTime.make+toUtc |
测试用例覆盖了这些转换式 Schema 的失败路径,例如(Schema.test.ts):
const schema = Schema.DateTimeZonedFromString // 编码:DateTime.Zoned → ISO + [IANA] 字符串 Schema.encodeSync(schema)(zoned) // => DateTime.formatIsoZoned(zoned) // 解码失败:非法字符串 Schema.decodeUnknownSync(schema)("invalid") // => ParseError: Expected a valid Zoned DateTime string再如TimeZoneFromString可同时解析"Europe/London"与"+03:00",而TimeZoneNamedFromString只接受 IANA 标识(Schema.test.ts):
const schema = Schema.TimeZoneFromString Schema.decodeUnknownSync(schema)("Europe/London") // => zoneMakeNamedUnsafe("Europe/London") Schema.decodeUnknownSync(schema)("+03:00") // => zoneMakeOffset(3 * 60 * 60 * 1000)这些转换式 Schema 内部均采用transformEffect(SchemaTransformation.transformEffect),解析失败时通过SchemaIssue.InvalidValue携带明确的期望信息(如"a valid Zoned DateTime string"、"a valid IANA time zone"、"a valid time zone"),可无缝融入 Effect 的管道式错误处理流程。
实战选型指南:何时用哪个 Schema?
综合源码中的版本划分与语义,给出如下选型建议:
- 持久化 / API 传输带时区的完整时刻→ 首选
DateTimeZoned(或解析字符串输入用DateTimeZonedFromString)。JSON 格式...+00:00[Europe/London]同时保留时刻与 DST 语义,比单纯偏移更可靠。 - 只需要 UTC 时刻→ 使用
DateTimeUtc/DateTimeUtcFromString/DateTimeUtcFromMillis,输出无歧义的 UTC ISO 字符串或 epoch 毫秒,体积最小。 - 单独建模时区字段(如"用户偏好时区"列)→ 用
TimeZoneNamed(限制为 IANA 命名时区)或TimeZone(允许偏移与命名并存,如"+08:00"与"Asia/Tokyo")。 - 需要任意值校验、不需要解析→ 用四个声明式 Schema(
TimeZoneOffset/TimeZoneNamed/TimeZone/DateTimeZoned),配合Schema.decodeSync使用;同时它们各自提供 JSON 序列化行为,可用于encode。 - 属性校验与测试数据生成→ 依赖 Arbitrary 生成:
TimeZoneNamed与TimeZone从五个常见 IANA 区域生成,DateTimeZoned在全日期域内联合同步生成时刻与时区,可无缝接入 effect/Schema 的 FastCheck 属性测试 流程。
在组合场景中,这些 Schema 可直接嵌入Schema.Struct,例如:
const UserPreferences = Schema.Struct({ createdAt: Schema.DateTimeZoned, // 完整带时区时刻 locale: Schema.String, timeZone: Schema.TimeZoneNamed // IANA 命名时区 })小结
本次 changeset 将DateTime模块的时区与带时区时刻类型正式纳入 Schema 体系,补齐了日期时间建模的关键一环。四个核心 Schema 各司其职:TimeZoneOffset以毫秒数字承载固定偏移、TimeZoneNamed以 IANA 字符串承载命名时区、TimeZone统一覆盖两种分支、DateTimeZoned则完整保留"时刻 + 时区"语义并以 ISO +[Zone]格式穿越 JSON 边界。配合 4.0.0 的*FromString系列与各 Schema 内置的 Arbitrary 生成规则,开发者可以放心地在 HTTP API、数据库持久化与属性测试场景中构建类型安全、时区正确的日期时间管线。相关实现与测试可继续阅读 Schema.ts 与 Schema.test.ts 获取一手依据。
【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考