PAYATHON 2026

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。

备注:内容仅供参考。