构造函数参数验证应早发现、早报错,推荐用独立验证函数封装逻辑,统一抛出错误,结合typescript类型系统与品牌类型、builder模式及schema校验提升可靠性与可维护性。

构造函数参数验证的核心是早发现、早报错,同时保持代码可读和可维护。不推荐在构造函数里堆砌大量 if 判断,也不建议把验证逻辑散落在各处。
用独立的验证函数封装校验逻辑
把参数检查抽成小而专注的函数,比如 requireNonEmptyString、requirePositiveNumber,每个只做一件事。构造函数里只需调用它们,语义清晰,复用方便。
- 验证失败时统一抛出
TypeError或自定义错误类(如InvalidArgumentError),便于上层捕获和区分 - 函数名体现意图,比如
assertValidEmail(email)比if (!email || !email.includes('@')) throw...更易懂 - 支持传入字段名或上下文,让错误信息包含具体参数名,调试更省力
利用 TypeScript 类型系统提前拦截
基础类型约束(如 string、number)和非空断言(name!: string)能挡住一部分问题,但不能替代运行时验证。真正关键的是配合类型守卫和 branded types。
- 对业务上有明确含义的值(如 ID、URL、金额),定义带品牌的类型:
type UserId = string & { readonly __brand: 'UserId' } - 通过工厂函数(如
createUserId(raw))做一次验证并返回 branded 类型,后续使用就天然受保护 - 避免在构造函数里直接接受原始类型,而是要求传入已验证的领域类型
延迟验证:用 builder 模式分步构建
当参数多、依赖关系复杂,或部分参数需异步获取时,直接在 constructor 里验证容易僵硬。改用 builder 模式,把验证分散到 setter 或 build 阶段。
- 每个 setter 可做局部校验(如格式、范围),build 方法做最终一致性检查(如“start 必须早于 end”)
- builder 实例本身可携带验证状态,支持
isValid()或getErrors()方法,方便表单等场景 - 避免构造函数承担过多职责,也降低单次初始化失败的风险
结合运行时 schema 验证(适合配置类对象)
对于结构较深、字段较多的配置对象,手动写验证易漏且难维护。可用 zod 或 yup 定义 schema,在构造前统一校验。
- schema 描述即文档,自动提供错误路径和提示,比手写判断更可靠
- 搭配 TypeScript,还能生成类型定义,实现“一次定义,类型+运行时双校验”
- 注意性能开销,高频创建对象时不建议每次调用;可缓存解析结果或仅在初始化/反序列化时使用
不复杂但容易忽略:验证不是越严越好,关键是贴合业务边界。一个 email 字段,校验是否为空、是否含 @ 符号通常够用,不必在构造时跑 DNS 查询。











