TypeScript中可选_id属性引发函数参数类型不兼容问题
说明:TypeClass是项目中处理的一种对象文档,实际称为typeclass,并非TypeScript中的type或class,特此说明避免混淆。
我开发了一个支持船舶TypeClass(类似汽车车型)新建和编辑的组件:当TypeClass包含_id时代表已有文档,需要调用接口更新数据库;无_id时仅更新本地状态。
相关核心代码如下:
const handleUpdateDeck = async (updatedDeck: FixedDeck | HoistableDeck) => { const updatedTypeClass = getUpdatedTypeClassWithNewDeck( typeClass, updatedDeck, selectedZoneID ); if (updatedTypeClass._id && "_id" in updatedTypeClass) { try { await handleUpdateTypeClass(updatedTypeClass); } catch (err: any) { console.log("Updating deck failed: ", err.message); } } else { setTypeClass(updatedTypeClass); } };
getUpdatedTypeClassWithNewDeck负责更新TypeClass中的deck数据,返回对象可能包含或不包含_id:
export default function ( typeClass: Omit<TypeClass, "_id"> & { _id?: string }, updatedDeck: FixedDeck | HoistableDeck, selectedZoneID: string ) { return { ...typeClass, zones: typeClass.zones.map((zone) => { if (zone._id === selectedZoneID) { const zoneToUpdate: Zone | undefined = typeClass.zones.find( (z) => z._id === selectedZoneID ); if (!zoneToUpdate) { throw new Error("failed to find zone to update"); } const updatedZone: Zone = { ...zoneToUpdate, decks: zoneToUpdate.decks.map((deck) => deck._id === updatedDeck._id ? updatedDeck : deck ), }; return updatedZone; } else { return zone; } }), }; }
handleUpdateTypeClass仅接收包含必填_id的TypeClass类型:
const handleUpdateTypeClass = async (tc: TypeClass) => { try { if (!updateTypeClass) { throw new Error("Update function undefined"); } if (!tc) { throw new Error("Couldn't find type-class"); } if (tc.name.length === 0) { throw new Error("name|Can't leave name empty"); } if (tc.zones.length === 0) { throw new Error("zones|Type-classes must have at least one zone"); } setErrors({}); setIsSubmitting(true); await updateTypeClass(tc); setTypeClass(tc); } catch (err: any) { throw new Error(err.message); } finally { setIsSubmitting(false); } };
TypeClass的基础类型定义:
export type TypeClass = { _id: string; // TypeClass名称 name: string; // 包含的区域列表 zones: Zone[]; }; export type Zone = { _id: string; // 区域名称 name: string; // 区域内的甲板列表(包含固定底层甲板,最少1个) decks: (FixedDeck | HoistableDeck)[]; };
组件中使用的TypeClass类型为:
typeClass: Omit<TypeClass, "_id"> & { _id?: string };
遇到的问题
我已经通过updatedTypeClass._id && "_id" in updatedTypeClass判断_id存在,但调用handleUpdateTypeClass时仍然触发类型错误:
Types of property '_id' are incompatible.
Type 'string | undefined' is not assignable to type 'string'.
Type 'undefined' is not assignable to type 'string'.ts(2345)
解决方法
方法1:直接使用类型断言
最简单的方式是在调用时断言updatedTypeClass为TypeClass类型:
await handleUpdateTypeClass(updatedTypeClass as TypeClass);
注意:这种方式需要确保运行时_id确实存在,否则可能引发运行时错误。
方法2:自定义类型守卫函数
编写类型守卫明确判断对象是否符合TypeClass类型,让TypeScript完全信任判断结果:
function isTypeClass(tc: Omit<TypeClass, "_id"> & { _id?: string }): tc is TypeClass { return typeof tc._id === 'string' && tc._id.length > 0; }
修改判断逻辑为:
if (isTypeClass(updatedTypeClass)) { try { await handleUpdateTypeClass(updatedTypeClass); } catch (err: any) { console.log("Updating deck failed: ", err.message); } } else { setTypeClass(updatedTypeClass); }
这种方式兼顾类型安全和运行时校验,是更推荐的方案。
方法3:优化工具函数的返回类型
给getUpdatedTypeClassWithNewDeck添加泛型,让TypeScript根据输入自动推断返回类型:
export default function getUpdatedTypeClassWithNewDeck<T extends Omit<TypeClass, "_id"> & { _id?: string }>( typeClass: T, updatedDeck: FixedDeck | HoistableDeck, selectedZoneID: string ): T { return { ...typeClass, zones: typeClass.zones.map((zone) => { if (zone._id === selectedZoneID) { const zoneToUpdate: Zone | undefined = typeClass.zones.find( (z) => z._id === selectedZoneID ); if (!zoneToUpdate) { throw new Error("failed to find zone to update"); } const updatedZone: Zone = { ...zoneToUpdate, decks: zoneToUpdate.decks.map((deck) => deck._id === updatedDeck._id ? updatedDeck : deck ), }; return updatedZone; } else { return zone; } }), }; }
此时如果输入的typeClass包含_id,返回的updatedTypeClass会被自动推断为TypeClass类型,无需额外判断即可直接调用handleUpdateTypeClass。
内容的提问来源于stack exchange,提问作者Tsabary

