sequelize-typescript icon indicating copy to clipboard operation
sequelize-typescript copied to clipboard

How can I use a model as a type?

Open shamoons opened this issue 5 years ago • 8 comments

I'm trying to do:

    let newEvent: Event = {
      id: (uuidv4() as unknown) as Guid,
      streamId: msg.streamId,
      data: msg.data,
      // response: undefined,
      // responseCode: undefined

    }

with Event being:

@Table
export class Event extends Model<Event> {
  @Column({ allowNull: false, primaryKey: true, type: DataType.UUID })
  id: Guid;

  @Column({ allowNull: false })
  data: string;

  @Column
  responseCode?: number;

  @Column
  response?: number;

  @ForeignKey(() => Stream)
  @Column({ allowNull: false })
  streamId: string;
}

but it says:

Type '{ id: Guid; streamId: any; data: any; }' is missing the following properties from type 'Event': $add, $set, $get, $count, and 28 more

shamoons avatar May 28 '20 21:05 shamoons

You should create a new object using the .create({}) function; not instantiate it like that. Assuming Stream is also an object, you probably want to add that with the .$add({}) magic function, and not add an id in the .create() function.

MagicLegend avatar May 31 '20 12:05 MagicLegend

But I’ll be adding to the object conditionally based on certain criteria

shamoons avatar May 31 '20 12:05 shamoons

Then either define a type in that class as an intermediary type, use an any type to build the object and then save it (after saving you have the typings again), or re-think your code logic to fit this style of working with the objects.

MagicLegend avatar May 31 '20 12:05 MagicLegend

In my model modules I'll export four items: the model, a "create" interface, an "update" interface, and sometimes a "read" interface. This is basically what I believe @MagicLegend meant by using an intermediary type, but a bit more extensive.

Example:

export interface UserRead {
  email: string;
  firstName: string;
  lastName: string;
}

@Table
export class User extends Model<User> implements UserRead {
  @AllowNull(false)
  @PrimaryKey
  @IsUUID(4)
  @Column
  userId!: string;

  @AllowNull(false)
  @Column
  email!: string;

  @AllowNull(false)
  @Column
  firstName!: string;

  @AllowNull(false)
  @Column
  lastName!: string;
}

export interface UserCreate {
  email: string;
  firstName: string;
  lastName: string;
  userId: string;
}

export interface UserUpdate {
  email?: string;
  firstName?: string;
  lastName?: string;
}

The class and each interface are carefully tuned to the needs of the model: sometimes fields exist in one or more of them that don't exist in the others. Often fields are required in some that are optional in others. It's all dependent on how I, as the database interface designer, need to provide restrictions the application should abide by to make sure the database is being used correctly - and thus reduce the amount of bugs I and my co-workers can easily create.

The "Read" interface I've only used for one task thus far: when I'm combining a subset of the data from the DB to some other object. So I currently only create it on a case-by-case basis.

I use the "Create" and "Update" interfaces thusly:

const userCreate: UserCreate = {
  email,
  firstName,
  lastName,
  userId,
};
await User.create(userCreate);

//...

const userUpdate: UserUpdate = {};
//... logic
userUpdate.firstName = "joe"; // Merely a fake example of course.
//... more logic
await User.update(userUpdate, { where: { userId } });

One of the rules I use is that the "Create" interface should have the same semantics as the create call would need: aka fields that cannot be null in the database means that the "Create" interface should use non-optional fields. This does mean that I often need some additional variables or advanced logic when creating the data:

const userId = uuid.v4();
const email = request?.email?.trim();

if (!email) { // All falsy values are bad.
  throw new Error("...");
}

const userCreate: UserCreate = {
  email,
  firstName: request?.firstName?.trim() ?? "Jo",
  lastName: request?.lastName?.trim() ?? "", // Example, not actually recommended.
  userId,
};
await User.create(userCreate);
// You probably should have all this in a transaction and detect if the create failed.

kf6kjg avatar Jun 08 '20 18:06 kf6kjg

I think you might be looking for build(). It will give you a new, unpersisted instance that you can then set attributes on:

const newEvent = Event.build({
  id: (uuidv4() as unknown) as Guid,
  streamId: msg.streamId,
  data: msg.data,
  // response: undefined,
  // responseCode: undefined
});

if (whatever) {
  newEvent.response = "something";
} 

sbleon avatar Jul 21 '20 14:07 sbleon

I also recently discovered the Partial, Omit, Pick, and related generic types.

So a direct solution for the OP could also be:

// Makes all keys optional.
const newEvent: Partial<Event> = {
      id: (uuidv4() as unknown) as Guid,
      streamId: msg.streamId,
      data: msg.data,
      // response: undefined,
      // responseCode: undefined
}

Or

// Selects only the keys specified.
const newEvent: Pick<Event, "id" | "streamId" | "data" | "response" | "responseCode"> = {
      id: (uuidv4() as unknown) as Guid,
      streamId: msg.streamId,
      data: msg.data,
      // response: undefined,
      // responseCode: undefined
}

However I still use the structure I posted before, just with some modifications: there's now a User_Base interface that all the other entries either extend or implement either directly or by modification via the above generics.

kf6kjg avatar Jul 21 '20 15:07 kf6kjg

Any solution?

pkhadson avatar Feb 22 '21 18:02 pkhadson

I'm using like this

import { Notifications } from 'src/modules/notifications/models/notifications.model';
import { InferAttributes } from 'sequelize';

const baseNotification: InferAttributes<Notifications> = {...}

"sequelize": "6.37.3"

lucas-yuri-deel avatar Apr 24 '24 12:04 lucas-yuri-deel