Sequelize+TypeScript关联字段访问错误:Property 'deviceInfo'不存在
我正在使用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时就会报错。
解决步骤:
统一关联别名
修改Device.hasMany的as参数,和类中声明的属性名保持一致:Device.hasMany(DeviceInfo, {as: 'deviceInfo', sourceKey: 'id', foreignKey: 'deviceId'});调整控制器的请求体类型
因为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}); } } };可选:修正
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

