Typegoose设置required的引用属性为何仍被判定可能为undefined
我正在使用Typegoose,以下是我的Invoice类定义:
export class Invoice extends StatusHistory { @prop({ required: true }) name!: string; }
以下是我的Contract类定义:
export class Contract extends StatusHistory { @prop({ required: true, ref: () => Invoice }) invoice!: Ref<Invoice>; }
在后端模型文件中将鼠标悬停在invoice属性上时,类型提示为:
(property)
Contract.invoice: Ref<Invoice, string>
我的项目为monorepo架构,类型在后端与Angular编写的前端之间共享。现假设合约服务中存在如下函数:
foo(contract: Contract): any { if (contract.invoice) { //do some stuff } }
此时在前端文件中将鼠标悬停在invoice属性上,类型提示变为:
(property)
Contract.invoice: Ref<Invoice, string | undefined>
我可以接受需要判断该属性是完整Invoice对象还是引用字符串的逻辑,但即便我明确知晓该字段为required、不存在未关联invoice的合约,每次使用invoice属性时仍需要先校验其不为undefined。
疑问点
- 为何在模型文件中该属性的类型不包含undefined,在前端文件中却包含该类型?
- 为何字段已配置required,Typegoose仍判定其可能为undefined?
- 如何配置才能让Typegoose识别该属性不可能为undefined,免去每次使用前的非空判断?
我已查阅Typegoose官方文档及公开网络搜索结果,未找到该特定场景的对应解决方案。
问题原因与解决方案
1. 前后端类型不一致原因
该差异由Typegoose类型推导逻辑、前后端TS编译配置差异共同导致:
- 后端项目通常已适配Typegoose装饰器的类型推导规则,
@prop配置required: true时,会在当前TS上下文自动将属性推导为非可选类型,因此最终类型为Ref<Invoice, string>,不包含undefined。 - Angular前端项目默认开启更严格的TS检查规则,且前端代码不会引入Typegoose装饰器的元数据运行时支持,TS无法识别
@prop({required: true})的类型修饰作用,只会按照类属性原始声明推导:由于invoice没有显式初始值,严格检查模式下TS会自动给属性类型追加undefined。
Ref<Invoice>默认第二泛型参数为string,当TS检测到属性未初始化时,会将该参数推导为string | undefined,就是前端看到的类型结果。
2. required配置不生效的原因
Typegoose的required: true配置仅作用于运行时的MongoDB文档校验,不会100%覆盖TS的静态类型检查结果。TS的静态类型推导完全依赖编译期的类型声明、TS配置规则,不会读取装饰器传入的配置参数来修改类型,除非显式给属性做明确的类型标注,或者使用Typegoose提供的内置类型工具修正推导结果。
3. 修复方案
根据monorepo共享类型场景,按优先级推荐以下方案:
- 方案一:显式标注Ref的泛型参数,固定非空类型
直接给Ref传入明确的第二泛型参数,强制指定引用ID的类型为非可选,从根源上避免TS在不同编译环境下追加undefined:
该写法不依赖任何TS配置或者装饰器元数据识别,前后端编译时都会统一识别为import { Ref } from "@typegoose/typegoose"; export class Contract extends StatusHistory { @prop({ required: true, ref: () => Invoice }) invoice!: Ref<Invoice, string>; // 显式指定第二泛型为string,不带undefined }string | Invoice类型,不会自动追加undefined。 - 方案二:统一monorepo内的TS严格检查配置
在共享类型包的tsconfig.json中统一设置strictPropertyInitialization: false,配合属性上已添加的非空断言!,可以减少跨端类型不一致问题,该方案需要配合方案一使用稳定性更高。 - 方案三:全局类型映射抹除可选属性(非必要不推荐)
如果项目中存在大量类似的required字段,可以写一个全局类型工具提取文档类的非空属性,适合前端拿到后端返回的、已经过校验的确定数据场景使用:type NonNullableDocument<T> = { [K in keyof T]: NonNullable<T[K]> } // 使用时 foo(contract: NonNullableDocument<Contract>): any { // 这里contract.invoice的类型会被强制推导为非空 }
不要为了省掉判断直接关闭整个项目的strictNullChecks,会大幅增加类型漏洞风险。
内容的提问来源于stack exchange,提问作者guaxuma

