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

Sequelize+TypeScript关联字段访问错误:Property 'deviceInfo'不存在

问题:Sequelize关联后控制器解构deviceInfo字段出现TypeScript编译错误

我正在使用Node.js + TypeScript + Express + Sequelize开发,尝试给device表和device info表建立关联,代码参考官方文档编写,但在控制器中从req.body解构deviceInfo字段时,出现如下TypeScript编译错误:

TSError: ⨯ Unable to compile TypeScript: src/controllers/deviceController.ts:26:47 - error TS2339: Property 'deviceInfo' does not exist on type 'Optional<InferCreationAttributes<Device, { omit: never; }>, NullishPropertiesOf<InferCreationAttributes<Device, { omit: never; }>>>'.


官方参考代码

class User extends Model<InferAttributes<User, { omit: 'projects' }>, InferCreationAttributes<User, { omit: 'projects' }>> {
  // id can be undefined during creation when using `autoIncrement`
  declare id: CreationOptional<number>;
  declare name: string;
  declare preferredName: string | null; // for nullable fields

  // timestamps!
  // createdAt can be undefined during creation
  declare createdAt: CreationOptional<Date>;
  // updatedAt can be undefined during creation
  declare updatedAt: CreationOptional<Date>;

  // Since TS cannot determine model association at compile time
  // we have to declare them here purely virtually
  // these will not exist until `Model.init` was called.
  declare getProjects: HasManyGetAssociationsMixin<Project>; // Note the null assertions!
  declare addProject: HasManyAddAssociationMixin<Project, number>;
  declare addProjects: HasManyAddAssociationsMixin<Project, number>;
  declare setProjects: HasManySetAssociationsMixin<Project, number>;
  declare removeProject: HasManyRemoveAssociationMixin<Project, number>;
  declare removeProjects: HasManyRemoveAssociationsMixin<Project, number>;
  declare hasProject: HasManyHasAssociationMixin<Project, number>;
  declare hasProjects: HasManyHasAssociationsMixin<Project, number>;
  declare countProjects: HasManyCountAssociationsMixin;
  declare createProject: HasManyCreateAssociationMixin<Project, 'ownerId'>;

  // You can also pre-declare possible inclusions, these will only be populated if you
  // actively include a relation.
  declare projects?: NonAttribute<Project[]>; // Note this is optional since it's only populated when explicitly requested in code

  // getters that are not attributes should be tagged using NonAttribute
  // to remove them from the model's Attribute Typings.
  get fullName(): NonAttribute<string> {
    return this.name;
  }

  declare static associations: {
    projects: Association<User, Project>;
  };
}

class Project extends Model<
  InferAttributes<Project>,
  InferCreationAttributes<Project>
> {
  // id can be undefined during creation when using `autoIncrement`
  declare id: CreationOptional<number>;

  // foreign keys are automatically added by associations methods (like Project.belongsTo)
  // by branding them using the `ForeignKey` type, `Project.init` will know it does not need to
  // display an error if ownerId is missing.
  declare ownerId: ForeignKey<User['id']>;
  declare name: string;

  // `owner` is an eagerly-loaded association.
  // We tag it as `NonAttribute`
  declare owner?: NonAttribute<User>;

  // createdAt can be undefined during creation
  declare createdAt: CreationOptional<Date>;
  // updatedAt can be undefined during creation
  declare updatedAt: CreationOptional<Date>;
}

...

Project.init(
  {
    id: {
      type: DataTypes.INTEGER.UNSIGNED,
      autoIncrement: true,
      primaryKey: true
    },
    name: {
      type: new DataTypes.STRING(128),
      allowNull: false
    },
    createdAt: DataTypes.DATE,
    updatedAt: DataTypes.DATE,
  },
  {
    sequelize,
    tableName: 'projects'
  }
);

User.init(
  {
    id: {
      type: DataTypes.INTEGER.UNSIGNED,
      autoIncrement: true,
      primaryKey: true
    },
    name: {
      type: new DataTypes.STRING(128),
      allowNull: false
    },
    preferredName: {
      type: new DataTypes.STRING(128),
      allowNull: true
    },
    createdAt: DataTypes.DATE,
    updatedAt: DataTypes.DATE,
  },
  {
    tableName: 'users',
    sequelize // passing the `sequelize` instance is required
  }
);

...

User.hasMany(Project, {
  sourceKey: 'id',
  foreignKey: 'ownerId',
  as: 'projects' // this determines the name in `associations`!
});

我的代码

// noinspection JSAnnotator

import sequelize from '../db';
import {
  Association,
  CreationOptional,
  DataTypes,
  ForeignKey,
  HasManyCreateAssociationMixin,
  HasManyGetAssociationsMixin,
  InferAttributes,
  InferCreationAttributes,
  Model,
  NonAttribute,
  Optional
} from 'sequelize';
import Type from './type';
import Brand from './brand';


export class Device extends Model<InferAttributes<Device, { omit: /*[*/'deviceInfo'/*, 'typeId', 'brandId']*/ }>, InferCreationAttributes<Device>> {
  declare id: CreationOptional<number>;
  declare name: string;
  declare price: number;
  declare rating: number | null;
  declare img: string;
  declare createdAt: CreationOptional<Date>;
  declare updatedAt: CreationOptional<Date>;
  declare getDeviceInfo: HasManyGetAssociationsMixin<DeviceInfo>;
  declare addDeviceInfo: HasManyCreateAssociationMixin<DeviceInfo, 'deviceId'>;
  declare deviceInfo: NonAttribute<DeviceInfo[]>;
  declare typeId: ForeignKey<Type['id']>

  declare static associations: {
    deviceInfo: Association<Device, DeviceInfo>,
    // typeId: Association<Device, Type>,
    // brandId: Association<Device, Brand>
  }
}

export class DeviceInfo extends Model<InferAttributes<DeviceInfo>, InferCreationAttributes<DeviceInfo>> {
  declare id: CreationOptional<number>;
  declare title: string;
  declare description: string;
  declare deviceId: ForeignKey<Device['id']>;
  declare device?: NonAttribute<Device>;
  declare createdAt: CreationOptional<Date>;
  declare updatedAt: CreationOptional<Date>;
}

DeviceInfo.init({
    id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
    title: { type: DataTypes.STRING, allowNull: false },
    description: { type: DataTypes.STRING, allowNull: false },
    createdAt: DataTypes.DATE,
    updatedAt: DataTypes.DATE,
  },
  {
    tableName: 'device_infos',
    sequelize
  });

Device.init({
  id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
  name: { type: DataTypes.STRING, unique: true, allowNull: false },
  price: { type: DataTypes.INTEGER },
  rating: { type: DataTypes.INTEGER, defaultValue: 0, allowNull: true },
  img: { type: DataTypes.STRING },
  createdAt: DataTypes.DATE,
  updatedAt: DataTypes.DATE,
}, {
  tableName: 'devices', sequelize
});

...

Device.hasMany(DeviceInfo, {as: 'device_infos', sourceKey: 'id', foreignKey: 'deviceId'});
DeviceInfo.belongsTo(Device, { foreignKey: 'deviceId' });

控制器代码

const deviceController = {
  create: async (req: Request<{}, {}, CreationAttributes<Device>>, res: Response) => {
    try {
      const {name, price, rating, id, typeId, deviceInfo } = req.body;
...

问题原因及解决方法

核心问题1:关联别名不匹配

你在Device.hasMany中设置的别名是'device_infos',但在Device类中声明的关联属性和associations里用的是deviceInfo,两者名称不统一。Sequelize会根据as参数关联属性名,导致TypeScript无法识别deviceInfo属于CreationAttributes<Device>的一部分。

核心问题2:deviceInfo是关联属性,不属于模型的创建属性

你在Device类中把deviceInfo标记为NonAttribute<DeviceInfo[]>,这意味着它不会被包含在InferCreationAttributes<Device>类型里,所以CreationAttributes<Device>自然没有这个字段。而你在控制器里把req.body的类型设为CreationAttributes<Device>,解构deviceInfo时就会报错。

解决步骤:

  1. 统一关联别名
    修改Device.hasMany的as参数,和类中声明的属性名保持一致:

    Device.hasMany(DeviceInfo, {as: 'deviceInfo', sourceKey: 'id', foreignKey: 'deviceId'});
    
  2. 调整控制器的请求体类型
    因为deviceInfo是关联数据,不属于Device本身的创建属性,需要自定义一个包含关联字段的请求体类型,而不是直接用CreationAttributes<Device>:

    // 自定义请求体类型
    interface CreateDeviceRequest extends CreationAttributes<Device> {
      deviceInfo?: InferCreationAttributes<DeviceInfo>[];
    }
    
    // 控制器中使用这个类型
    const deviceController = {
      create: async (req: Request<{}, {}, CreateDeviceRequest>, res: Response) => {
        try {
          const {name, price, rating, id, typeId, deviceInfo } = req.body;
          // 先创建Device
          const device = await Device.create({name, price, rating, id, typeId});
          // 再批量创建DeviceInfo并关联
          if (deviceInfo) {
            await Promise.all(
              deviceInfo.map(info => DeviceInfo.create({...info, deviceId: device.id}))
            );
          }
          // 返回结果
          res.status(200).json(device);
        } catch (error) {
          res.status(500).json({message: '创建设备失败', error});
        }
      }
    };
    
  3. 可选:修正Device类的泛型参数
    你当前Device类的泛型里InferAttributes的omit参数被注释了,其实可以明确把deviceInfo排除,避免类型混淆:

    export class Device extends Model<InferAttributes<Device, { omit: 'deviceInfo' }>, InferCreationAttributes<Device>> {
      // ... 其他声明
      declare deviceInfo?: NonAttribute<DeviceInfo[]>; // 加上可选标记,因为只有主动include才会有值
    }
    

这样修改后,TypeScript就能正确识别req.body中的deviceInfo字段,编译错误也会消失,同时关联逻辑也能正常工作。

内容的提问来源于stack exchange,提问作者user23356564

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 19:18:10