TypeScript中as Partial<T>断言时如何禁止非法键与拼写错误
TypeScript 严格部分属性类型校验实现方案
我最初需要实现两个工具类型:其一为值类型与原类型匹配的类型子集,其二仅要求键存在于目标类型中。我初步写出了如下实现,但不确定TypeScript 4.7版本是否已提供内置能力。
更新说明
核心痛点并非工具类型本身,而是类型断言的校验漏洞:使用{ nammme: 'John' as any } as Partial<Person>这类写法时可以绕过类型检查,这是需要拦截的场景。需求为:仅允许覆盖目标类型的部分属性(无需覆盖全部属性),同时拦截键名拼写错误的非法属性。
问题复现
业务场景(Firestore更新)
// 符合预期:写法正确,可通过类型校验 const fields: Partial<DbUser> = { organizationId: FieldValue.delete() as any, groups: FieldValue.delete() as any, }; return getFirestore() .collection(constants.dbCollections.users) .doc(context.auth.uid) .update(fields); // 不符合预期:存在拼写错误typoooo,却能通过类型校验 return getFirestore() .collection(constants.dbCollections.users) .doc(context.auth.uid) .update({ organizationId: FieldValue.delete() as any, groups: FieldValue.delete() as any, typoooo: 1 as any, } as Partial<DbUser>);
测试用例
type Person = { name: string; age: number; }; export type KeysIn<T> = { [key in keyof Partial<T>]: any; }; export type MustContainKeyButTypeOfKeyCanBeOverwritten<T> = unknown; // 待实现的目标类型 // 合法:键存在于Person类型中 const valid1: KeysIn<Person> = { name: 0, }; // 合法:键存在于Person中且值类型匹配 const valid2: Partial<Person> = { name: '', }; // 非法:键不存在于Person中 const invalid1: KeysIn<Person> = { x: true, }; // 非法:键存在于Person中但值类型不匹配 const invalid2: Partial<Person> = { name: 0, }; // 非法:键不存在于Person中 const invalid3: Partial<Person> = { x: true, }; // 带any的拼写错误场景 const invalid4: KeysIn<Person> = { namessss: '' as any, }; const invalid5 = { namessss: '' as any, } as Partial<Person>; // 目前可通过校验,不符合预期 const invalid6 = { namessss: '' as any, } as KeysIn<Person>; // 目前可通过校验,需要拦截 // 目标类型理想效果 const idealExample: MustContainKeyButTypeOfKeyCanBeOverwritten<Person> = { name: 1 as any, // 允许显式覆盖已存在键的类型 aggggggge: 1, // 键名拼写错误,需报错 age: '2', // 值类型不匹配且未显式as any,需报错 };
问题根因
as类型断言本身是TypeScript提供给开发者的强制类型转换能力,设计上就不会执行严格的多余属性校验,只要两个类型存在结构重叠就允许断言通过。TypeScript仅在对象字面量直接赋值给显式标注类型的变量时才会触发严格的多余属性检查,一旦使用as断言、或先将对象赋值给无类型标注的中间变量,该校验就会失效。
直接修改工具类型无法解决as断言绕过校验的问题,需要通过泛型约束在类型推断阶段完成严格校验。
实现代码
1. 核心类型定义
/** * 严格部分类型:禁止多余键,未显式标注any时值类型必须与原类型匹配 * 满足MustContainKeyButTypeOfKeyCanBeOverwritten的需求 */ type StrictPartial<T, U> = { [K in keyof U]: K extends keyof T ? T[K] : never; }; /** * 宽松键校验类型:禁止多余键,允许任意覆盖已存在键的值类型 */ type StrictKeysOnly<T, U> = { [K in keyof U]: K extends keyof T ? any : never; };
2. 泛型包装函数(用于自动推断校验)
/** * 创建严格Partial校验器 * 用法:const obj = strictPartial<TargetType>()({ ... }) */ function strictPartial<T>() { return <U extends Partial<T>>(obj: StrictPartial<T, U>): U => obj; } /** * 创建仅键名校验器 * 用法:const obj = strictKeysOnly<TargetType>()({ ... }) */ function strictKeysOnly<T>() { return <U extends Record<PropertyKey, any>>(obj: StrictKeysOnly<T, U>): U => obj; }
使用效果
修复Firestore更新场景
// 校验通过 return getFirestore() .collection(constants.dbCollections.users) .doc(context.auth.uid) .update(strictPartial<DbUser>()({ organizationId: FieldValue.delete() as any, groups: FieldValue.delete() as any, })); // 编译期直接报错:typooooo不存在于DbUser类型中 return getFirestore() .collection(constants.dbCollections.users) .doc(context.auth.uid) .update(strictPartial<DbUser>()({ organizationId: FieldValue.delete() as any, groups: FieldValue.delete() as any, typoooo: 1 as any, }));
满足理想示例要求
// 校验通过:name显式as any允许覆盖类型 const validCase = strictPartial<Person>()({ name: 1 as any, }); // 编译报错:aggggggge不存在于Person类型 const invalidTypo = strictPartial<Person>()({ name: 1 as any, aggggggge: 1, }); // 编译报错:age类型为string,与Person中定义的number不匹配 const invalidType = strictPartial<Person>()({ name: 1 as any, age: '2', });
内容的提问来源于stack exchange,提问作者Jonathan
相关产品推荐
相关产品推荐

