05 null、undefined、可选属性与默认值
对应项目:FindCustomersOptions 和 PATCH 输入。当前配置启用 strict、exactOptionalPropertyTypes、noUncheckedIndexedAccess。
1. 三种“可能没有邮箱”的类型不同
interface OptionalEmail { email?: string }
interface PresentButMaybeUndefined { email: string | undefined }
interface NullableEmail { email: string | null }
| 类型 | 属性可缺失? | 显式 undefined? | null? |
|---|---|---|---|
email?: string |
可以 | 当前 exactOptionalPropertyTypes 下通常不可以写入 | 不可以 |
email: string | undefined |
不可以 | 可以 | 不可以 |
email: string | null |
不可以 | 不可以 | 可以 |
如果需要同时允许缺失和显式 undefined,应写 email?: string | undefined;不要习惯性地把所有可选字段都扩大,先确定契约。
读取 OptionalEmail.email 仍然是 string | undefined,因为属性可能没有。严格可选属性配置主要约束“写入”,不是消除“读取时缺失”的可能。
2. PATCH 必须区分不修改与清空
interface UpdateCustomerBody {
email?: string | null;
}
语义:
{}:不修改 email。{ email: "ann@example.com" }:更新邮箱。{ email: null }:清空邮箱。
function buildPatch(input: UpdateCustomerBody): UpdateCustomerBody {
const patch: UpdateCustomerBody = {};
if (input.email !== undefined) patch.email = input.email;
return patch;
}
不能用 if (input.email),否则 null 被跳过,清空请求不生效。把 null 改成 undefined 也会丢失业务意图。
JSON 本身没有 undefined 值。网络请求中的省略与 null 要明确区分;服务内部的 JS 对象还能含 undefined,因此规范化仍有必要。
3. 为什么直接赋可选值会报错?
interface Input { email?: string | null }
function buildInput(source: Input): Input {
return { email: source.email }; // 可能写入 undefined,当前配置报错
}
推荐按是否提供构造:
function buildInput(source: Input): Input {
return {
...(source.email !== undefined ? { email: source.email } : {}),
};
}
或显式 if 后赋值。当前 Customer Controller 构造嵌套输入时就是这样做的,既保留 null,也不把未提供字段写成 undefined。
这是有意表达“属性不存在”和“存在但值为 undefined”的差别,不是编译器无缘无故挑剔。
4. ?? 与 ||:false 和 0 不一定是缺省
const enabled = false;
enabled ?? true; // false
enabled || true; // true
const page = 0;
page ?? 1; // 0,应交给合法性检查拒绝
page || 1; // 1,错误输入被悄悄变成默认值
?? 只处理 null/undefined。|| 处理所有假值,包括 false、0、空字符串、NaN。
用哪个不是语法偏好,而是“业务把哪些值视为缺失”。即使选了 ??,也不表示输入允许 null;验证层可以拒绝 null。
5. 解构默认值只处理 undefined
function normalize(input: { active?: boolean | null }) {
const { active = true } = input;
return active;
}
normalize({}); // true
normalize({ active: false }); // false
normalize({ active: null }); // null
解构默认值不替换 null。需要把 null/undefined 都变默认,可明确写 input.active ?? true;若 null 是非法输入,则应拒绝而不是默认。
6. ?. 避免异常,不保证数据完整
interface CustomerView { address?: { city?: string } }
function getCity(customer: CustomerView): string {
return customer.address?.city; // 错误:可能是 undefined
}
选择取决于业务:
- 城市合理缺失:返回类型改成 string | undefined/null。
- 展示允许占位文案:
return customer.address?.city ?? "未知"。 - 这次查询必须加载城市:检查后抛内部错误,让调用者修正 include。
不要到处补空字符串,掩盖本该完整的数据。可选链解决访问安全,不解决契约缺失。
7. 数组非空判断后,为什么 items[0] 仍可能报错?
function firstName(names: string[]): string {
if (names.length === 0) throw new Error("没有名字");
return names[0]; // 当前配置下仍可能是 string | undefined
}
TS 对普通数组长度与每个索引的关系推断有限。最简单的解法是检查实际要使用的值:
function firstName(names: string[]): string {
const first = names[0];
if (first === undefined) throw new Error("没有名字");
return first;
}
若函数契约确实只接受非空数组,可定义元组 [string, ...string[]]。但普通 string[] 不能不经检查直接变成非空元组。
遍历数组通常更方便:for…of 中的元素按元素类型处理。对数组可能存放 undefined 的情况,还要单独表达和检查。
8. 环境变量也属于这一类边界
process.env.DB_NAME 是 string | undefined,TS 不读取 .env 证明它存在。
应该读取到局部变量,检查后返回确定的 string。保留的专题见 process.env 教程;里面的 DATABASE 等变量名是教学示例,当前工程实际使用 DB_NAME 等配置名。
同理,JWT 附加属性、SQL 第一行、关联数据都要按各自边界确认存在。
9. 练习
- 为 PATCH 邮箱分别测试缺失、null、有效字符串、空字符串。
- 比较
{ email: undefined }与{}在 exactOptionalPropertyTypes 下的赋值结果。 - 给空数组取首项,分别尝试长度判断、元素判断与非空元组。
- 给 active:false 和 page:0 使用不同默认逻辑,解释结果是否符合业务。
下一章:函数返回值与执行路径。