Effect Duration.Input 升级:DurationObject 支持 Temporal 风格对象输入的实现解析
【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code
本文基于 t3code 仓库内嵌的 effect-smol 仓库(位于.repos/effect-smol/)中一份真实的 changeset 变更说明展开,系统讲解Duration输入模型新增DurationObject这一 Temporal 风格对象输入的动机、类型定义、源码级解码实现与测试验证。读完本篇,你可以掌握 EffectDuration.Input的完整输入形态,理解对象式时长(如{ hours: 1, minutes: 30 })如何被精确换算、舍入与规范化,并能在自己的 Effect 项目里正确选用fromInput与fromInputUnsafe两个转换入口。
变更背景:一份 changeset 说明了什么
本次讲解的核心文档是 t3code 仓库内 effect-smol 仓库的变更说明文件 duration-temporal-object-input.md,其原文内容如下:
--- "effect": patch --- Add `DurationObject` to `Duration.Input` to support Temporal-style object input. Durations can now be created from objects with named unit properties like `{ hours: 1, minutes: 30 }`, similar to `Temporal.Duration.from()`. Supported fields: `weeks`, `days`, `hours`, `minutes`, `seconds`, `millis`, `micros`, `nanos`.从这份 changeset 可以直接读出三个关键事实:
- 变更级别为 patch:frontmatter 中
"effect": patch表明这是对effect包的兼容性增强,不破坏既有 API; - 目标是扩展
Duration.Input联合类型:新增成员DurationObject,让"带命名字段的时间单位对象"成为合法输入,语义上对标 ECMAScript Temporal 提案中的Temporal.Duration.from(); - 字段集合:changeset 列出了
weeks、days、hours、minutes、seconds、millis、micros、nanos八类命名单位。
需要注意的一个细节:changeset 的措辞早于最终实现,其中亚毫秒单位写作millis/micros/nanos,而当前仓库源码中实际落地的接口字段名是milliseconds、microseconds、nanoseconds(见下文类型定义)。本文一律以当前仓库源码为准。
这份变更目前处于 effect-smol 的 4.0.0 预发布(pre)流程中——pre.json 显示"mode": "pre", "tag": "rc",说明DurationObject相关能力随 4.0.0 rc 版本线发布,类型声明中也标注了@since 4.0.0。
DurationObject 类型定义与完整 Input 联合类型
DurationObject的完整定义位于 Duration.ts:
export interface DurationObject { readonly weeks?: number | undefined readonly days?: number | undefined readonly hours?: number | undefined readonly minutes?: number | undefined readonly seconds?: number | undefined readonly milliseconds?: number | undefined readonly microseconds?: number | undefined readonly nanoseconds?: number | undefined }所有字段均为可选且相互叠加(additive):任意子集组合都合法,{ seconds: 1 }、{ days: 1, hours: 2 }或仅{ nanoseconds: 500 }都能构造出有效时长。接口注释明确写道 "Compatible with Temporal.Duration-like objects"(见 Duration.ts L182-L212),即设计目标是让持有 Temporal 风格时长对象的代码可以直接传入 Effect API。
DurationObject作为新成员被并入Duration.Input联合类型(Duration.ts L172-L180),完整的输入形态如下:
| 输入形态 | TypeScript 类型 | 语义 |
|---|---|---|
| 已有时长 | Duration | 原样返回 |
| 毫秒数 | number | 按毫秒解释 |
| 纳秒数 | bigint | 按纳秒解释 |
| 高精度二元组 | readonly [seconds: number, nanos: number] | 秒 + 纳秒,对齐hrtime风格 |
| 时长字符串 | `${number} ${Unit}` | 如"10 seconds",单位见Unit类型 |
| 无穷字符串 | "Infinity"/"-Infinity" | 正/负无穷 |
| 时长对象(本次新增) | DurationObject | 命名单位字段叠加 |
其中Unit类型(Duration.ts L129-L145)支持单复数混写,如nano/nanos、micro/micros、milli/millis直至week/weeks。
解码实现:fromInputUnsafe 的对象分支
所有输入形态最终由fromInputUnsafe统一解码。其对象分支(Duration.ts L287-L318)是本次变更的核心,源码逻辑可归纳为四层:
第一层:Duration 实例短路。若对象上带有 Duration 的 TypeId("~effect/time/Duration"),直接返回原实例,不做任何换算:
if (TypeId in input) return input as Duration第二层:二元组分支。数组输入按[seconds, nanos]解释,带完整的边界处理:长度不为 2 或非数字字段时走invalid抛错;两个分量含NaN时返回zero;任一分量为-Infinity返回负无穷,为Infinity返回正无穷;否则以roundTiesAwayFromZero(input[0] * 1_000_000_000 + input[1])归一化为纳秒。
第三层:DurationObject 字段叠加(本次新增)。各命名单位先被折算到毫秒整数轴上(Duration.ts L305-L317):
const obj = input as DurationObject let millis = 0 // we can use truthy checks here, because 0 can be ignored if (obj.weeks) millis += obj.weeks * 604_800_000 // 1 周 = 7 * 86_400_000 if (obj.days) millis += obj.days * 86_400_000 // 1 天 = 24 * 3_600_000 if (obj.hours) millis += obj.hours * 3_600_000 if (obj.minutes) millis += obj.minutes * 60_000 if (obj.seconds) millis += obj.seconds * 1_000 if (obj.milliseconds) millis += obj.milliseconds if (!obj.microseconds && !obj.nanoseconds) return make(millis) return make(roundTiesAwayFromZero( millis * 1_000_000 + (obj.microseconds ?? 0) * 1_000 + (obj.nanoseconds ?? 0) ))这里有三个值得注意的实现细节:
- truthy 检查而非
!= null:源码注释说明0值可以安全忽略,因为0 * 单位对累加结果无影响,代码因此保持简洁; - 快速路径:当输入不含亚毫秒字段(
microseconds/nanoseconds)时,直接以纯毫秒值调用make(millis),避免一次大整数乘法与舍入; - 亚毫秒精度路径:一旦存在
microseconds或nanoseconds,整体换算到纳秒轴——毫秒部分乘以1_000_000、微秒部分乘以1_000、纳秒直接相加——再交给roundTiesAwayFromZero做四舍五入到最近纳秒(ties away from zero,平局远离零方向舍入)。
第四层:非法输入兜底。走到分支末尾仍未匹配任何形态的输入会落入invalid(input),抛出Invalid Input: ...错误(Duration.ts L323-L325)。
roundTiesAwayFromZero本身定义在 Duration.ts L38-L39:
const roundTiesAwayFromZero = (input: number): bigint => BigInt(input < 0 ? Math.ceil(input - 0.5) : Math.floor(input + 0.5))即对正数向下加 0.5 取整、对负数向上加 0.5 取整,保证±0.5平局时统一向绝对值增大方向舍入。这一规则与Input类型文档中 "Finite fractional values that are normalized to nanoseconds are rounded to the nearest nanosecond, with ties away from zero" 的声明一致。
类型文档中的行为示例
DurationObject接口的 jsdoc 内嵌了三个行为示例(Duration.ts L190-L198):
import { Duration } from "effect" Duration.fromInputUnsafe({ seconds: 30 }) // => Duration.seconds(30) Duration.fromInputUnsafe({ days: 1 }) // => Duration.days(1) Duration.fromInputUnsafe({ seconds: 1, nanoseconds: 500 }) // => Duration.nanos(1_000_000_500n)第三个示例值得品味:{ seconds: 1, nanoseconds: 500 }的结果是Nanos 形态(1_000_000_500n)而非 Millis 形态——由于存在亚毫秒成分,整条换算被提升到纳秒轴,最终时长保留了500n纳秒的尾部精度。这说明对象的输出形态由输入是否含亚毫秒字段决定,而不是固定输出毫秒。
安全入口 fromInput 与错误边界
fromInputUnsafe的语义是"输入可信、非法即抛错"。当输入来源不可信(如用户配置、远端请求体)时,应使用fromInput,它通过Option.liftThrowable将抛错转换为Option(Duration.ts L343-L345):
export const fromInput: (u: Input) => Option.Option<Duration> = Option.liftThrowable( fromInputUnsafe )文档给出的示例行为:
Duration.fromInput(1000) // => Option.some(Duration.seconds(1)) Duration.fromInput("invalid" as any) // => Option.none()从源码结构看,fromInput对DurationObject输入同样适用:一个字段名拼写错误(例如误用 changeset 早期措辞里的millis)的对象,虽然不会抛错(未知字段被忽略、合法字段照常累加),但会导致时长静默变短——这是对象式输入相比字符串输入更需要配套 Schema 校验的原因。在 Effect 生态中,这类边界通常由调用方在Schema层完成字段白名单校验后,再交给fromInput做兜底转换。
测试验证
effect-smol 的测试文件 Duration.test.ts 中包含针对对象输入的断言,例如:
deepStrictEqual(Duration.fromInputUnsafe({ seconds: 30 }), Duration.seconds(30))(Duration.test.ts L76),以及多字段叠加的场景(Duration.test.ts L108):
Duration.fromInputUnsafe({ days: 1, hours: 2, minutes: 30, seconds: 15 })测试覆盖了"单一字段与等价构造器一致"和"多字段叠加"两条主线,与上文源码中millis累加逻辑一一对应。
变更溯源与相关配套改动
从 CHANGELOG.md 可以确认该变更的合并信息:
- PR #1696(commit
5a84853,贡献者 @krzkaczor):"AddDurationObjecttoDuration.Inputto support Temporal-style object input",正文与 changeset 文件逐字一致,即本文开头的字段清单就是该 PR 的发布说明; - 同一条变更线还包含PR #1701(commit
21d5d5e):"allow assigning Temporal types to DateTime & Duration input"——即在同一批 4.0.0 rc 变更中,DateTime与Duration的输入类型同步放宽了对 Temporal 类型赋值的接受度。两者组合后,Effect 的时间 API 在类型层面形成了一套与 Temporal 提案对齐的输入契约。
在项目中如何使用与适用前提
以当前仓库源码为准,在 effect-smol 4.0.0 rc 之后的版本中,任何接受Duration.Input的 Effect API(延迟、超时、TTL、调度间隔等)都可以直接传入命名单位对象:
import { Duration, Effect } from "effect" // Temporal 风格对象输入 const backoff = Duration.fromInputUnsafe({ minutes: 5, seconds: 30 }) // 等价于 5 分 30 秒的毫秒时长 // 含亚毫秒精度时输出纳秒形态 const precise = Duration.fromInputUnsafe({ seconds: 1, nanoseconds: 500 }) // => Duration.nanos(1_000_000_500n) // 不可信输入走安全路径 const parsed = Duration.fromInput({ hours: 1, milliseconds: 250 })适用前提与限制需要明确:
- 版本前提:
DurationObject与扩展后的Input联合类型均标注@since 4.0.0(Duration.ts L170),且 effect-smol 当前处于rc预发布模式(pre.json),尚未到稳定版的项目应在升级前留意 rc 阶段可能存在的接口微调; - 字段名以源码为准:亚毫秒字段名为
milliseconds/microseconds/nanoseconds,changeset 文本中的millis/micros/nanos是早期措辞; - 未知字段被静默忽略:解码只读取白名单字段,拼写错误不会报错,建议在上游用 Schema 或
Struct校验字段集合; - 叠加语义:所有字段为正负相加,负数分量合法并参与远离零方向的舍入。
参考文件索引
| 文件 | 作用 |
|---|---|
| duration-temporal-object-input.md | 本次变更的 changeset 原文(patch 级别说明) |
| Duration.ts | DurationObject、Input类型与fromInputUnsafe/fromInput实现 |
| Duration.test.ts | 对象输入的测试断言 |
| CHANGELOG.md | PR #1696/#1701 的合并记录 |
| pre.json | effect-smol 当前处于 pre/rc 发布模式的配置 |
【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考