Angular signal 和 input 的初始值应该用 null 还是 undefined?
结论
题目中的两种 signal 写法都可以使用。选择时,先确定空值在业务中代表什么,再遵循项目已有约定。仅凭它是 signal 的初始值,无法决定必须用 null 还是 undefined。
如果项目没有现成约定,可以采用以下规则:
- 用
undefined表示尚未提供值,或某个可选值缺失。 - 用
null表示业务上明确没有对象,例如当前没有选中项。 - 如果需要区分加载中、加载完成但无结果等状态,单独建模这些状态。
对于没有默认值的可选 input,可以直接写 input<MyCustomType>(),其值类型包含 undefined。
为什么“尚未初始化”不能直接决定答案?
下面两个 signal 都已经完成初始化:
const nullableValue = signal<MyCustomType | null>(null);
const optionalValue = signal<MyCustomType | undefined>(undefined);
第一个当前保存 null,第二个当前保存 undefined。显式传入 undefined 的 signal 同样已经初始化。Angular 的 signal API 要求提供 initialValue,这两种写法都满足要求。
“未提供值用 undefined,明确无值用 null”是一种语义约定,变量的具体业务含义仍需由代码约定。Angular 也不会仅凭这两个值判断数据是否正在加载、请求是否失败,或用户是否清除了选择。
普通 signal 应该怎样选择?
先检查业务类型和数据来源,再统一空值的表示方式。例如,下面两种声明表达了不同的约定:
// 业务含义:当前没有选中项。
const selectedItem = signal<MyCustomType | null>(null);
// 业务含义:数据尚未提供。
const pendingItem = signal<MyCustomType | undefined>(undefined);
这里假定 MyCustomType 是已经定义的业务类型。
如果接口和现有代码已经统一使用 null 表示空结果,沿用 null 通常更容易维护。如果周边 API 普遍使用 undefined 表示缺失值,选择 undefined 可以减少转换。没有业务区别时,团队统一使用其中一种即可,不必同时引入两种空值。
如果需要表达多个状态,可以使用可辨识联合类型。例如:
type ItemState<T> =
| { status: 'loading' }
| { status: 'empty' }
| { status: 'ready'; value: T };
const itemState = signal<ItemState<MyCustomType>>({
status: 'loading',
});
// 加载完成,但没有结果。
itemState.set({ status: 'empty' });
调用方可以直接判断 status,不用猜测某个空值代表哪个阶段。
input 的选择有什么不同?
input 还涉及组件对调用方的输入约定。以下是组件或指令类中的字段声明,分别适用于不同需求:
// 可选输入;未绑定时为 undefined。
optionalItem = input<MyCustomType>();
// 可选输入;未绑定时使用初始值 null。
nullableItem = input<MyCustomType | null>(null);
// 必填输入;调用方需要提供绑定。
requiredItem = input.required<MyCustomType>();
input<MyCustomType>() 返回的类型是 InputSignal<MyCustomType | undefined>。如果输入是可选的,可以直接使用这个形式;如果组件用 null 表示没有对象,可以显式设置 null;如果调用方必须提供输入,则使用 input.required。相关行为见 Angular input API。
必填输入应在 Angular 提供输入值后读取,不能把 input.required 当作创建时就已有业务数据的普通 signal。
使用时要注意什么?
建议开启 TypeScript 的 strictNullChecks,让编译器检查空值分支。开启后,null 和 undefined 是不同的类型,使用业务对象前需要完成相应检查。参见 TypeScript strictNullChecks 文档。
读取 signal 后,可以先保存当前值再判断:
const item = selectedItem();
if (item !== null) {
// 此处分支中的 item 类型为 MyCustomType。
}
不要为了消除类型错误而直接使用非空断言 !,它不会在运行时补上缺失的数据。团队约定也应说明空值的含义、允许出现的位置和处理方式,这比单独规定“全部用 null”或“全部用 undefined”更有帮助。
备注:内容仅供参考。
