sequelize/sequelize-typescript

Typing errors when using nested creates with associations

Opened this issue · 4 comments

Issue

Following methods do not accept an object with an association,

  • Model.create()

Versions

  • "sequelize": "^6.37.6"
  • "sequelize-typescript": "^2.1.6"

My tsconfig has no strict mode settings enabled. Interestingly, this problem doesn't occur when strictNullChecks is enabled. My tsconfig:

{
  "compilerOptions": {
    "outDir": "./dist",
    "allowJs": true,
    "module": "commonjs",
    "target": "ES2020",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  },
  "include": ["./src/**/*"]
}

Explanation

I am using sequelize and typescript in a project. I am receiving an error when trying to create a new instance of a model while including one of its hasOne associations. Relevant documentation on the matter: https://sequelize.org/docs/v6/advanced-association-concepts/creating-with-associations/.

// Model A
  export class ModelA extends Model<
    InferAttributes<ModelA>,
    InferCreationAttributes<ModelA>
  > {
  // ... Other fields/columns, skipping for brevity 

  @HasOne(() => ModelB, { onDelete: "SET NULL" })
  modelB: ModelB; 
  }

// Model B
  export class ModelB extends Model<
    InferAttributes<ModelB>,
    InferCreationAttributes<ModelB>
  > {
    @BelongsTo(() => ModelA)
    modelA?: ModelA;
    }

And when I try to create an instance of ModelA, including ModelB:

await ModelA.create({...propertiesOfModelA, 
          modelB: {...propertiesOfModelB}},
          {include: [ModelB],})

I am met with this error:

Type '<fields of modelB argument>' is missing the following properties from type 'ModelB': $add, $set, and 35 more.ts(2740)

ModelA(90, 3): The expected type comes from property 'modelB' which is declared here on type 'Optional<InferCreationAttributes<ModelA, { omit: never; }>, NullishPropertiesOf<InferCreationAttributes<ModelA, { omit: never; }>>>'

I do not expect typescript to enforce that the modelB field in the values argument contain every single field, property, and method of an instance of ModelB. I can remove the stricter typing like this:

export class ModelA extends Model {}

But then Typescript has no way to enforce the types inside of the values argument passed into the create method.

I could typecast, but this feels like a bandaid:

     await ModelA.create({...propertiesOfModelA, modelB: {...propertiesOfModelB} as ModelB,
     {include: [ModelB],})

I could also define the type of ModelA's modelB field as partial, but this incorrectly communicates that all fields and columns in ModelBare optional (even if some are specified as required/not nullable).

    @HasOne(() => ModelB, {onDelete: "SET NULL"})
    modelB: Partial<ModelB>

Even if I were to define ModelB as such, the error persists:

export class ModelB extends Model<Partial<ModelB>> {}

My goal is to maintain type strictness in all relevant model methods (specifically create), so that I can still create models with associations and ensure I'm passing in the correct types.

I got the problem solved like this. Made the solution up by myself, it is not documented anywhere.

export class User extends Model<
  InferAttributes<User>,
  InferCreationAttributes<User> & {
    preferences: CreationAttributes<Preferences>;
    profile: CreationAttributes<Profile>;
  }
>

For your example:

export class ModelA extends Model<
  InferAttributes<ModelA>,
  InferCreationAttributes<ModelA> & {
    modelB: CreationAttributes<ModelB>;
  }
> 

I hope this helps. Also, if there are better solutions, please let me know.

@jleweli Out of curiosity, does your project have strict or strictNullChecks enabled in your tsconfig?

@hdeleon99 No, currently strictNullChecks is not enabled.

@jleweli Interesting. I've noticed that Sequelize behaves "properly" when I enable strictNullChecks. In other words, with strictNullChecks enabled, Sequelize does not enforce that the nested create field is an actual instance of Model.