先记住这个答案
将 unknown 变成业务对象,应先确认非空对象,再检查所需属性是否存在、字段值的类型和业务限制,最后返回经过整理的结果。简单边界可以用普通解析函数,复用判断也可以写类型谓词,但谓词实现必须与它承诺的完整类型一致。只检查一个标签就断言整个复杂对象,仍可能遗漏嵌套字段、数组元素和数值范围。解析失败应有明确错误路径,成功结果可以重新构造,避免把未知额外字段和共享可变引用直接带入内部。
- 先证明容器可检查,再逐个证明字段可用
- 类型谓词的返回签名不会替作者验证实现
- 解析函数可以同时校验并构造干净的内部对象
对象检查需要拆成可证明的步骤
typeof 的 object 分支仍包含 null,数组也属于对象,因此具体协议如果不接受数组,应额外排除。通过容器检查以后,属性存在只能证明这个键可读,不能证明它的值就是数字。把每一步证据写清楚,编译器与维护者才能跟上解析过程。
示例只承诺返回一个正整数用户标识和非空名称。它验证了容器、两个字段的基础类型以及必要的业务范围,再构造一个新对象。额外字段不会被自动复制,非法输入则立即进入明确的错误路径,调用方无需在每次读取时重复猜测结构。
type UserSummary = { id: number; name: string };
function parseUser(value: unknown): UserSummary {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new TypeError('user must be an object');
}
if (!('id' in value) || typeof value.id !== 'number' ||
!Number.isSafeInteger(value.id) || value.id <= 0) {
throw new TypeError('invalid user id');
}
if (!('name' in value) || typeof value.name !== 'string' || !value.name.trim()) {
throw new TypeError('invalid user name');
}
return { id: value.id, name: value.name.trim() };
}合法对象会得到仅包含两个字段的新结果,空值、数组、非整数标识和空白名称会被拒绝。这里按普通解析数据设计,若输入可能是带副作用的访问器或 Proxy,还需要限定输入来源与读取方式。
类型谓词为什么也可能撒谎
返回 value is UserSummary 的函数会让编译器信任判断结果,但函数体并不会自动被证明等价于整个接口。实现如果永远返回 true,后续代码依然可能获得错误的安全感。谓词既要考虑成功分支,也要确认失败分支所表达的排除是否合理。
复杂结构包含数组和嵌套对象时,要继续验证元素和子字段。Array.isArray 只证明容器是数组,不能证明每项都是用户。若规则多、错误提示需要定位字段路径,可以使用明确的解析器组合方式,避免一个巨大布尔表达式难以审查和维护。
校验后是否还会失去保证
如果返回原对象引用,其他持有者随后修改字段,先前校验建立的事实可能失效。按需要重新构造不可变视图或复制必要层级,可以减少这种共享写入问题;但浅复制只隔离顶层,嵌套数据仍应根据契约处理。
校验失败时可以抛错,也可以返回带成功标签的结果联合,关键是调用者必须处理失败。接口边界的错误信息应帮助定位字段与原因,同时避免把整个敏感输入原样写进日志。解析器测试应同时覆盖合法样本、缺字段、错误类型和业务边界。
容易答错的地方
- 检查 typeof 是 object 就能当业务接口
- 对象分支还可能包含空值与数组,具体字段和领域限制也未得到证明;需要按后续实际读取的契约逐步检查。
- 类型谓词有类型签名就不会写错
- 编译器会信任谓词所声明的关联,不能自动证明所有业务条件;应审阅实现并用错误输入验证每个承诺的字段。
面试官还会怎么问?
校验函数必须返回原输入对象吗?
不必,解析函数可以返回清理后的新对象,统一格式并丢弃不需要的字段;如果要保留引用身份,应明确后续可变性与所有权。
属性存在检查会接受继承属性吗?
in 会沿原型链检查,因此不是自有属性证明;普通 JSON 数据通常没有自定义原型来源,其他输入则需要额外明确自有属性政策。
为什么已经校验 number 还检查整数范围?
number 包含小数、无穷和非数等值,而业务标识通常要求正的安全整数;基础类型证明与领域有效性检查承担不同责任。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。