0%

错误记录:TypeScript推断类型导致的never错误及解决方案

AI导读:在开发三维场景管理系统时,遇到 TypeScript 类型推断导致的错误。问题发生在显式定义类型为 Set<SceneObject> 时,由于 SceneObjectES3DTileset 可能具有相同的私有属性 _id,导致 TypeScript 在进行类型缩减时将它们的交集推导为 never 类型,从而无法访问某些属性。解决方案包括使用类型断言、定义类型守卫、修改类型定义、使用联合类型或放宽类型约束。最佳实践推荐使用类型守卫或联合类型,以平衡类型安全性和代码简洁性。

错误记录:TypeScript 推断类型导致的 never 错误及解决方案

问题背景

在开发一个三维场景管理系统时,我遇到一个奇怪的 TypeScript 报错。以下是两段核心代码片段:

第一段代码:工作正常

1
2
3
4
5
myProjectManager.sceneObjectsManager.sceneObjects.forEach((sceneObject) => {
if (sceneObject instanceof ES3DTileset) {
sceneObject.allowPicking = false;
}
});

第二段代码:报错

1
2
3
4
5
6
7
const notES3DTilesetPick = (sceneObjects: Set<SceneObject>) => {
sceneObjects.forEach((sceneObject) => {
if (sceneObject instanceof ES3DTileset) {
sceneObject.allowPicking = false; // 报错
}
});
};

TypeScript 报错如下:

1
2
Property 'allowPicking' does not exist on type 'never'.
The intersection 'SceneObject & ES3DTileset' was reduced to 'never' because property '_id' exists in multiple constituents and is private in some.

问题分析

1. 为什么第一段代码正常?

在第一段代码中,TypeScript 能够正常推断 sceneObject 的类型。可能的原因包括:

  • 类型未显式定义sceneObjectsManager.sceneObjects 的类型是宽泛的,比如 Set<any>Set<SceneObject | ES3DTileset>。在这种情况下,instanceof 类型缩减可以正常生效。
  • 推断宽松:如果没有显式类型定义,TypeScript 会尽量宽松处理,并允许 instanceof 动态缩减类型。
  • 未检查私有属性冲突:TypeScript 不会强制检查类型中可能存在的私有属性冲突。

2. 为什么第二段代码报错?

在第二段代码中,sceneObjects 的类型显式定义为 Set<SceneObject>,导致 TypeScript 在进行 instanceof 类型缩减时触发了更严格的检查。

导致报错的具体原因

  • 私有属性冲突SceneObjectES3DTileset 类型可能都定义了私有属性 _id,即使属性名称和类型一致,TypeScript 也认为它们是冲突的,因为 private 属性是由声明类独占的。
  • **交集缩减为 never**:TypeScript 尝试将 SceneObjectES3DTileset 的交集作为缩减后的类型,但由于 _id 的冲突,交集被简化为 never,导致无法访问 allowPicking

总结

TypeScript 的类型检查机制更严格,显式类型声明要求更明确的类型兼容性,导致了错误。


解决方案

1. 使用类型断言

最简单的解决方法是告诉 TypeScript 我们确信 sceneObjectES3DTileset 类型:

1
2
3
4
5
6
7
const notES3DTilesetPick = (sceneObjects: Set<SceneObject>) => {
sceneObjects.forEach((sceneObject) => {
if (sceneObject instanceof ES3DTileset) {
(sceneObject as ES3DTileset).allowPicking = false; // 类型断言
}
});
};

2. 定义类型守卫

创建一个类型守卫函数,明确告知 TypeScript sceneObjectES3DTileset

1
2
3
4
5
6
7
8
9
10
11
const isES3DTileset = (obj: SceneObject): obj is ES3DTileset => {
return obj instanceof ES3DTileset;
};

const notES3DTilesetPick = (sceneObjects: Set<SceneObject>) => {
sceneObjects.forEach((sceneObject) => {
if (isES3DTileset(sceneObject)) {
sceneObject.allowPicking = false;
}
});
};
  • 这种方式更加语义化,适合大型项目。

3. 修改类型定义

如果可以修改 SceneObjectES3DTileset 的定义,确保它们的 private 成员一致,或者将冲突的 private 属性改为 protected

1
2
3
4
5
6
7
class SceneObject {
protected _id: string;
}

class ES3DTileset extends SceneObject {
allowPicking: boolean;
}

4. 使用联合类型

修改 sceneObjects 的类型定义为联合类型,避免交集冲突:

1
2
3
4
5
6
7
const notES3DTilesetPick = (sceneObjects: Set<SceneObject | ES3DTileset>) => {
sceneObjects.forEach((sceneObject) => {
if (sceneObject instanceof ES3DTileset) {
sceneObject.allowPicking = false;
}
});
};

5. 宽松类型约束

如果对类型安全要求较低,可以将 sceneObjects 定义为宽松类型:

1
2
3
4
5
6
7
const notES3DTilesetPick = (sceneObjects: Set<unknown>) => {
sceneObjects.forEach((sceneObject) => {
if (sceneObject instanceof ES3DTileset) {
(sceneObject as ES3DTileset).allowPicking = false;
}
});
};

最佳实践

推荐使用 类型守卫联合类型,这两种方法既能解决问题,又能保留 TypeScript 的类型安全性:

  • 类型守卫:适合需要显式区分类型的复杂场景。
  • 联合类型:适合能够确定对象来源的场景。

总结

这次问题源于 TypeScript 对类型推断的严格性。当类型定义显式声明为 Set<SceneObject> 时,交集类型检查因私有属性冲突而失败;而未显式声明类型时,TypeScript 自动宽松推断类型,从而避免了问题。

通过调整类型定义、使用类型断言或类型守卫,可以灵活解决此类问题。在开发中,应根据实际需求选择解决方案,既保证类型安全,也能简化代码逻辑。

欢迎关注我的其它发布渠道