TypeScript 技巧：使用语义化时间告别“毫秒 vs 秒”的单位换算烦恼

每个 API 都有自己的脾气。setTimeout 用毫秒，Redis TTL 通常用秒，JWT 过期时间也是秒，但某些数据库的时间戳又是毫秒。这种不一致性不仅增加了心智负担，还极其容易引发 Bug（比如把 10 秒当成 10 毫秒，或者反过来）。与其每次写代码时都在心里做乘除法，不如利用 TypeScript 的模板字面量类型，构建一套语义化时间系统。 1. 核心实现：通用的 ms 解析器 // utils/time.ts type Unit = "ms" | "s" | "m" | "h" | "d"; // 利用 TS 模板字面量类型，限制格式必须如 "10s", "100ms", "1d" export type Duration = `${number} ${Unit}` | `${number}${Unit}`; /** * 将人类可读的时间字符串转换为毫秒 (Milliseconds) */ export function ms(d: Duration): number { const match = d.match(/^(\d+)\s?(ms|s|m|h|d)$/); if (!match) { throw new Error(`Unable to parse duration: ${d}`); } const time = Number.parseInt(match[1]); const unit = match[2] as Unit; switch (unit) { case "ms": return time; case "s": return time * 1000; case "m": return time * 1000 * 60; case "h": return time * 1000 * 60 * 60; case "d": return time * 1000 * 60 * 60 * 24; default: throw new Error(`Unknown unit: ${unit}`); } } /** * 如果 API 需要的是“秒”，直接套用这个 wrapper * 解决了一次性的心智负担，调用处无需再手动 / 1000 */ export function seconds(d: Duration): number { return ms(d) / 1000; } 2. 举个栗子 // ❌ 容易混淆的写法 checkLimit({ capacity: 100, interval: 86400000 // 这是多少？一天？还是十天？毫秒还是微秒？ }); 使用了语义化类型后，代码的可读性和维护性瞬间提升： import { ms, type Duration } from "./utils/time"; export type RateLimitOptions = { capacity: number; // 直接在类型定义中强制使用语义化字符串 interval: Duration; cost?: number; }; export function checkLimit(options: RateLimitOptions) { // 在函数内部统一转换为毫秒处理，外部调用者无感知 const intervalMs = ms(options.interval); console.log(`限流窗口大小: ${intervalMs}ms`); // ... 具体的令牌桶或漏桶逻辑 } checkLimit({ capacity: 5000, interval: "1h", // 清晰明了：1小时 }); checkLimit({ capacity: 10, interval: "30s", // 清晰明了：30秒 }); 总结通过引入 Duration 类型和 ms 解析器，我们获得了三个好处：消除歧义：不再需要猜测 86400 到底是秒还是毫秒。类型安全：TypeScript 会阻止你传入 "10 lightyears" 这种非法单位。心智解脱：写代码时想到“10分钟”，直接写 "10m"，不用在脑子里做 10 * 60 * 1000 的算术题。