JSON 转 TypeScript:null、字段缺失、optional 与 union
JSON 中没有 undefined:字段可以完全缺失,也可以存在且值为 null。TypeScript 的 ? 表示属性可能不存在,string | null 表示属性存在时可以是 null,二者组合后仍与 undefined 不同。本文只讨论这四种状态的建模,不讨论 OpenAPI 代码生成。
一、问题概述:缺失和 null 不是同一种输入
{} 与 {"nickname":null} 在 JSON 中不同。前者没有 nickname 键,后者明确传递空值;如果类型只写 nickname: string | null,缺失字段就不符合模型;只写 nickname?: string,又无法表达服务端明确返回 null。
二、最小复现:optional 与 nullable 的组合
下面的类型把三种情况分开:必需数字、必需但可为 null 的 nickname,以及可缺失且存在时可为 null 的 note。开启 strictNullChecks 后,编译器会要求调用方处理这些分支。
type Profile = {
id: number;
nickname: string | null;
note?: string | null;
};
const explicitNull: Profile = { id: 1, nickname: null };
const missingOptional: Profile = { id: 2, nickname: "Ada" };
console.log(Object.hasOwn(explicitNull, "note")); // false
console.log(Object.hasOwn(missingOptional, "nickname")); // true三、根因:TypeScript 属性访问会把缺失表现为 undefined
读取可选属性时,运行时结果可能是 undefined;读取必需的 string | null 属性时,结果只能在 string 与 null 之间。JSON.stringify 还会省略值为 undefined 的对象属性,因此把 undefined 当成 JSON null 会改变传输契约。
四、推荐方案:先定义传输状态,再写 TypeScript 类型
先列出字段是否必需、是否允许 null、是否允许其他类型。缺失与 null 都有业务意义时使用 field?: T | null,只允许缺失不允许 null 时使用 field?: T,必须出现但可为空时使用 field: T | null。不要用宽泛的 any 掩盖状态差异。
五、完整代码:显式区分缺失、null 与实际值
以下函数返回一个状态标签,使用 Object.hasOwn 区分缺失和显式 null。它不把空字符串、0 或 false 当成缺失。
type Input = { nickname?: string | null; count: number | null };
type FieldState = "missing" | "null" | "value";
function nicknameState(input: Input): FieldState {
if (!Object.hasOwn(input, "nickname")) return "missing";
return input.nickname === null ? "null" : "value";
}
console.log(nicknameState({ count: 0 })); // missing
console.log(nicknameState({ nickname: null, count: 0 })); // null
console.log(nicknameState({ nickname: "", count: 0 })); // value六、常见错误方案
用 value || defaultValue 会把空字符串、0 和 false 当成缺失;用 value ?? defaultValue 虽然保留了空字符串,但仍会把显式 null 和缺失合并;把所有可选字段都写成 any 则失去编译期提示。
七、边界条件:嵌套字段、数组和 union 收窄
嵌套对象的父字段缺失时,不能直接读取子字段;数组元素也可能分别缺失或为 null。union 需要通过 typeof、字面量比较或自定义类型守卫收窄,不能只靠类型断言绕过检查。
八、如何验证接口契约
准备字段缺失、显式 null、空字符串、0、false 和正常值的 JSON 样本。分别检查 Object.hasOwn、运行时值、JSON.stringify 输出和 TypeScript 编译错误;若接口要求省略 null 字段,应在序列化层明确实现。
九、FAQ
问:field?: string 等于 field: string | undefined 吗?答:访问结果可能相似,但 optional 还描述了对象键是否存在,exactOptionalPropertyTypes 等配置会影响赋值规则。
问:null 和 undefined 可以互换吗?答:JSON 传输中 undefined 不会作为值出现,不能未经契约转换。
问:union 是否意味着所有类型都要接受?答:是,调用方必须在使用前通过控制流或类型守卫收窄。
十、总结
字段缺失、null、optional 和 union 分别描述存在性、空值和允许的类型集合。用 ?、| null 和明确的类型守卫表达传输契约,保留 0、false、空字符串等真实值,并用边界 JSON 与 strictNullChecks 验证。
来源与延伸阅读
技术审校所依据的规范与权威参考资料。
相关文章
OpenAPI/Swagger Schema 到 TypeScript 契约:代码生成与运行时校验
使用 OpenAPI 术语说明 schema、required、nullable、请求/响应契约到 TypeScript 的生成边界,并区分静态代码生成与运行时校验。
实现原理JSON 转 TypeScript 类型推断引擎:递归、数组合并与命名
从 JSON 样本递归推断 TypeScript 类型,处理对象字段、数组元素合并和非法标识符命名,并明确样本推断的局限。
踩坑避坑JSON 大整数精度丢失:Number 安全范围与解决方案
说明 JSON 大整数进入 JavaScript 后为什么会被舍入,演示 Number.MAX_SAFE_INTEGER 边界,并给出字符串契约、BigInt 转换和序列化的可验证方案。
继续阅读
可打开关联的浏览器工具,使用自己的样本验证文中的处理流程。
打开关联工具