如何基于继承Model的接口创建排除id的DataObject类型?
解决TypeScript中DataObject的严格属性约束问题
需求说明
需要实现DataObject<T>类型,满足以下要求:
- 仅接受继承自
Model的类型T - 自动排除
T中的id属性(数据库自动生成,插入时无需传入) - 强制包含
T除id外的所有必填属性 - 禁止传入
T未定义的额外属性
现有代码实现
type DataObject<T extends Model> = Omit<T, "id"> interface Model extends JSONObject { id: string } interface JSONObject { [key: string]: JSONValue } type JSONArray = JSONValue[] type JSONValue = PrimitiveValue | JSONObject | JSONArray type PrimitiveValue = string | number | boolean // 插入数据库函数 function insert<T extends Model>(identifier: string, data: DataObject<T>) { // 插入数据逻辑 }
当前问题
上述实现未达到预期约束效果:
- 传入
T未定义的额外属性(如示例中的foo)不会触发类型报错 - 缺少
T除id外的必填属性不会报错 - 甚至允许传入
id属性也无校验提示
示例错误代码(当前无报错,但理应禁止):
interface Person extends Model { name: string, age: number } // 应报错:缺少age、存在额外属性foo和id insert<Person>("person", { foo: "bar", id: "a1b2" }) // 应报错:存在id属性 const personData: DataObject<Person> = { name: "John", id: "12345" }
问题根源
Model继承了带有索引签名的JSONObject,导致Omit<T, "id">会保留这个索引签名。TypeScript对带有索引签名的类型会放宽校验:允许任意额外属性,且不会强制要求显式声明的必填属性(因为索引签名覆盖了属性约束)。
解决方案
方案1:拆分Model结构(推荐)
将Model拆分为基础无索引签名的接口和带JSON结构的联合类型,分离插入和查询场景的类型约束:
// 基础Model接口,仅包含id,无索引签名 interface BaseModel { id: string } // 查询返回的Model类型(带JSON结构) type Model = BaseModel & JSONObject // 保留原JSON相关定义 interface JSONObject { [key: string]: JSONValue } type JSONArray = JSONValue[] type JSONValue = PrimitiveValue | JSONObject | JSONArray type PrimitiveValue = string | number | boolean // 严格约束的DataObject:排除id,禁止额外属性 type DataObject<T extends BaseModel> = Omit<T, "id"> & { [K in Exclude<keyof JSONObject, keyof Omit<T, "id">>]?: never } // 调整插入函数的泛型约束为BaseModel function insert<T extends BaseModel>(identifier: string, data: DataObject<T>) { // 插入数据逻辑 } // 示例使用 interface Person extends BaseModel { name: string, age: number } // 现在会触发预期报错:缺少age、存在foo和id属性 insert<Person>("person", { foo: "bar", id: "a1b2" }) // 报错:存在id属性 const personData: DataObject<Person> = { name: "John", id: "12345" } // 合法调用 insert<Person>("person", { name: "John", age: 30 })
方案2:自定义StrictOmit类型
如果不想拆分Model结构,可以通过自定义类型移除索引签名并禁止额外属性:
// 自定义严格排除类型:移除指定属性,同时禁止额外属性 type StrictOmit<T, K extends keyof T> = { [P in Exclude<keyof T, K>]: T[P] } & { [key: string]: never } // 调整DataObject为StrictOmit type DataObject<T extends Model> = StrictOmit<T, "id"> // 保留原Model及相关定义 interface Model extends JSONObject { id: string } interface JSONObject { [key: string]: JSONValue } type JSONArray = JSONValue[] type JSONValue = PrimitiveValue | JSONObject | JSONArray type PrimitiveValue = string | number | boolean function insert<T extends Model>(identifier: string, data: DataObject<T>) { // 插入数据逻辑 } // 测试同样会触发预期报错 interface Person extends Model { name: string, age: number } insert<Person>("person", { foo: "bar", id: "a1b2" }) // 报错 const personData: DataObject<Person> = { name: "John", id: "12345" } // 报错
关键原理
- 移除类型中的索引签名,让TypeScript启用严格的属性校验
- 通过
[key: string]: never约束禁止任何未定义的额外属性 - 保留原类型的必填属性约束,确保所有非id的必填字段都被传入
内容的提问来源于stack exchange,提问作者Noobster
相关产品推荐
相关产品推荐

