You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于继承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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 04:20:22