TypeScript 可选属性的类型推断为何导致赋值错误?
开启 strictNullChecks 后,读取可选属性 field1?: TField1 得到的类型是 TField1 | undefined,而 this.field1 的类型是 TField1。当 TField1 为 string 等不包含 undefined 的类型时,编译器就无法确认这次赋值是安全的。
可以根据字段是否允许缺失来调整声明,无需使用类型断言:允许调用方省略字段,就让实例属性接受 undefined;实例属性必须是 TField1,就要求调用方提供字段,或在构造时补上有效值。
泛型默认值为什么没有解决问题?
TField1 = undefined 表示,当调用方没有显式指定类型参数,类型推断也无法选出候选类型时,使用 undefined 作为默认类型。
它并不要求省略 field1 时,TField1 就必须是 undefined。例如,原接口允许这样写:
const options: ProcessedResultOptions<string> = {};
这里已经明确指定 TField1 为 string,但 field1 是可选属性,所以空对象仍然合法。读取 options.field1 时,实际值是 undefined,类型是 string | undefined。
原来的构造参数声明也允许这样的调用:
new ProcessedResult<string>({});
如果实例的 field1 声明为 string,构造函数却把缺失属性的值直接赋给它,类型声明就会与实际值不一致。
为什么不按每次调用的推断结果检查构造函数?
下面这个调用没有提供可用于推断的字段值,也没有其他类型信息,因此会使用默认类型:
new ProcessedResult({});
// 类型参数使用默认值:undefined、undefined
泛型类的实现需要对所有合法的类型参数成立。TypeScript 不会根据这一次调用,就把构造函数里的 TField1 当成 undefined 来检查。
检查构造函数时,TField1 仍然可能是调用方指定的任意类型:
this.field1 = options.field1;
// 左侧:TField1
// 右侧:TField1 | undefined
因此,编译器无法保证右侧满足左侧的类型要求。const 类型参数影响的是推断时对字面量类型的保留,也无法消除这里的 undefined。
方案一:允许字段缺失,实例属性包含 undefined
如果字段缺失时,实例属性就应该是 undefined,可以直接修改实例属性的类型:
export interface ProcessedResultOptions<
TField1 = undefined,
TField2 = undefined
> {
field1?: TField1;
field2?: TField2;
}
export class ProcessedResult<
const TField1 = undefined,
const TField2 = undefined
> {
public readonly field1: TField1 | undefined;
public readonly field2: TField2 | undefined;
constructor(options: ProcessedResultOptions<TField1, TField2>) {
this.field1 = options.field1;
this.field2 = options.field2;
}
}
此时,实例属性的类型能涵盖参数允许的情况:
const result = new ProcessedResult<string>({});
result.field1;
// 类型:string | undefined
// 实际值:undefined
不过,即使某次调用传入了 field1,实例属性的类型仍然包含 undefined。类声明对所有调用提供相同的类型保证,构造函数不会根据某次调用是否包含字段,自动调整实例属性的类型。
方案二:实例属性必须为 TField1,参数属性改为必填
如果希望保留以下实例属性声明:
public readonly field1: TField1;
public readonly field2: TField2;
可以去掉参数属性上的 ?:
export interface ProcessedResultOptions<
TField1 = undefined,
TField2 = undefined
> {
field1: TField1;
field2: TField2;
}
读取必填属性 field1 得到的类型是 TField1,原来构造函数中的赋值就能通过检查。
调用方需要提供两个属性,没有值的字段可以显式传入 undefined:
const result = new ProcessedResult({
field1: "hello",
field2: undefined,
});
此时,new ProcessedResult<string>({}) 会在调用处报错。泛型参数有默认类型,也不会让必填属性自动变成可选属性。
如果仍要允许调用方省略字段,同时保证实例属性是 TField1,就需要在字段缺失时提供一个属于 TField1 的默认值,或抛出异常终止构造。构造函数无法为任意的 TField1 凭空生成这样的值。
类型断言和编译选项需要注意什么?
options.field1 as TField1 只会让编译器按指定类型处理表达式,不会检查或改变运行时的值。如果调用方指定 TField1 为 string,却省略了字段,使用断言后,实例仍可能把 undefined 暴露为 string。
exactOptionalPropertyTypes 也无法解决这次赋值的问题。它主要影响能否向可选属性显式写入 undefined。只要属性允许缺失,读取时就仍需考虑 undefined。
备注:内容仅供参考。
