From f8dfa3b23fad165594d1f5e72f0c91d15fbee0bc Mon Sep 17 00:00:00 2001 From: Guylian Cox Date: Wed, 5 Jan 2022 13:30:44 +0100 Subject: [PATCH 01/21] feat(types): add `AttributesOf` utility type --- types/lib/model.d.ts | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/types/lib/model.d.ts b/types/lib/model.d.ts index bbff030fd2ec..646e9823cdce 100644 --- a/types/lib/model.d.ts +++ b/types/lib/model.d.ts @@ -2926,3 +2926,41 @@ export type ModelDefined = ModelStatic>; export type ModelStatic = NonConstructor & { new(): M }; export default Model; + +/** + * Utility type to extract Attributes of a given Model. + * + * It returns all instance properties defined in the Model, except: + * - those inherited from Model (intermediate inheritance works), + * - the ones whose type is a function, + * - the ones manually excluded using the second parameter. + * + * It cannot detect whether something is a getter or not, you should use the `Excluded` + * parameter to exclude getter & setters from the attribute list. + * + * @example + * // listed attributes will be 'id' & 'firstName'. + * class User extends Model> { + * id: number; + * firstName: string; + * } + * + * @example + * // listed attributes will be 'id' & 'firstName'. + * // we're excluding the `name` & `test` getters using the second argument. + * class User extends Model> { + * id: number; + * firstName: string; + * + * get name() { return this.firstName; } + * get test() { return ''; } + * } + */ +export type AttributesOf = { + [Key in keyof M as + M[Key] extends Fn ? never + : Key extends keyof Model ? never + : Key extends Excluded ? never + : Key + ]: M[Key] +}; From 0351311b217b09efdc0d89333c125cf090d0a9f3 Mon Sep 17 00:00:00 2001 From: Guylian Cox Date: Wed, 5 Jan 2022 13:56:41 +0100 Subject: [PATCH 02/21] docs: document & typecheck `AttributesOf` --- docs/manual/other-topics/typescript.md | 219 ++++++++++++++++++ types/lib/model.d.ts | 6 +- .../ModelInitWithAttributesOf.ts | 197 ++++++++++++++++ 3 files changed, 420 insertions(+), 2 deletions(-) create mode 100644 types/test/typescriptDocs/ModelInitWithAttributesOf.ts diff --git a/docs/manual/other-topics/typescript.md b/docs/manual/other-topics/typescript.md index 39b9c87a636c..bac894c2e22e 100644 --- a/docs/manual/other-topics/typescript.md +++ b/docs/manual/other-topics/typescript.md @@ -296,6 +296,225 @@ async function doStuffWithUserModel() { } ``` +### Using with `AttributesOf` + +`AttributeOf` is a utility type you can use to automatically extract attributes from your class definition. +It's an alternative solution designed to reduce the boilerplate of the above solution. + +Here is the same example as above, but using `AttributeOf`. + +Some caveats of this solution: + +- it's not possible to use `AttributesOf` for `CreationAttributes`. + This example opted for making properties that are not set until saved always optional instead. +- `AttributesOf` cannot whether a field is a getter, so they are not excluded by default. Use the second parameter to exclude getters. + eg. `AttributesOf` will exclude the field "projects" from the attribute list. +- `AttributesOf` excludes fields that are inherited from `Model`. + If one of your attributes shares the same name as one of the properties of `Model`, change its name. + Doing this is likely to cause issues anyway. + +**NOTE:** Keep the following code in sync with `/types/test/typescriptDocs/ModelInitWithAttributesOf.ts` to ensure it typechecks correctly. + +```typescript +/** + * Keep this file in sync with the code in the "Using with `AttributesOf`" section in typescript.md + */ +import { + Association, DataTypes, HasManyAddAssociationMixin, HasManyCountAssociationsMixin, + HasManyCreateAssociationMixin, HasManyGetAssociationsMixin, HasManyHasAssociationMixin, Model, + ModelDefined, Optional, Sequelize, AttributesOf +} from 'sequelize'; + +const sequelize = new Sequelize('mysql://root:asd123@localhost:3306/mydb'); + +// 'projects' is excluded as it's not an attribute, it's an association. +class User extends Model> { + // can be undefined during creation when using `autoIncrement` + declare id?: number; + declare name: string; + declare preferredName: string | null; // for nullable fields + + // timestamps! + // can be undefined during creation + declare readonly createdAt?: Date; + // can be undefined during creation + declare readonly updatedAt?: 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; // Note the null assertions! + declare addProject: HasManyAddAssociationMixin; + declare hasProject: HasManyHasAssociationMixin; + declare countProjects: HasManyCountAssociationsMixin; + declare createProject: HasManyCreateAssociationMixin; + + // You can also pre-declare possible inclusions, these will only be populated if you + // actively include a relation. + declare readonly projects?: Project[]; // Note this is optional since it's only populated when explicitly requested in code + + declare static associations: { + projects: Association; + }; +} + +// You can write `extends Model, AttributesOf>` instead, +// but that will do the exact same thing as below +class Project extends Model> { + // id can be undefined during creation when using `autoIncrement` + declare id?: number; + declare ownerId: number; + declare name: string; + + // can be undefined during creation + declare readonly createdAt?: Date; + // can be undefined during creation + declare readonly updatedAt?: Date; +} + +class Address extends Model> { + declare userId: number; + declare address: string; + + // can be undefined during creation + declare readonly createdAt?: Date; + // can be undefined during creation + declare readonly updatedAt?: Date; +} + +// You can also define modules in a functional way +interface NoteAttributes { + id: number; + title: string; + content: string; +} + +// You can also set multiple attributes optional at once +interface NoteCreationAttributes + extends Optional { +} + +Project.init( + { + id: { + type: DataTypes.INTEGER.UNSIGNED, + autoIncrement: true, + primaryKey: true + }, + ownerId: { + type: DataTypes.INTEGER.UNSIGNED, + allowNull: false + }, + name: { + type: new DataTypes.STRING(128), + allowNull: false + }, + }, + { + 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 + } + }, + { + tableName: 'users', + sequelize // passing the `sequelize` instance is required + } +); + +Address.init( + { + userId: { + type: DataTypes.INTEGER.UNSIGNED + }, + address: { + type: new DataTypes.STRING(128), + allowNull: false + } + }, + { + tableName: 'address', + sequelize // passing the `sequelize` instance is required + } +); + +// And with a functional approach defining a module looks like this +const Note: ModelDefined = sequelize.define( + 'Note', + { + id: { + type: DataTypes.INTEGER.UNSIGNED, + autoIncrement: true, + primaryKey: true + }, + title: { + type: new DataTypes.STRING(64), + defaultValue: 'Unnamed Note' + }, + content: { + type: new DataTypes.STRING(4096), + allowNull: false + } + }, + { + tableName: 'notes' + } +); + +// Here we associate which actually populates out pre-declared `association` static and other methods. +User.hasMany(Project, { + sourceKey: 'id', + foreignKey: 'ownerId', + as: 'projects' // this determines the name in `associations`! +}); + +Address.belongsTo(User, { targetKey: 'id' }); +User.hasOne(Address, { sourceKey: 'id' }); + +async function doStuffWithUser() { + const newUser = await User.create({ + name: 'Johnny', + preferredName: 'John', + }); + console.log(newUser.id, newUser.name, newUser.preferredName); + + const project = await newUser.createProject({ + name: 'first!' + }); + + const ourUser = await User.findByPk(1, { + include: [User.associations.projects], + rejectOnEmpty: true // Specifying true here removes `null` from the return type! + }); + + // Note the `!` null assertion since TS can't know if we included + // the model or not + console.log(ourUser.projects![0].name); +} + +(async () => { + await sequelize.sync(); + await doStuffWithUser(); +})(); +``` + ## Usage of `sequelize.define` In Sequelize versions before v5, the default way of defining a model involved using `sequelize.define`. It's still possible to define models with that, and you can also add typings to these models using interfaces. diff --git a/types/lib/model.d.ts b/types/lib/model.d.ts index 646e9823cdce..c03801c2144a 100644 --- a/types/lib/model.d.ts +++ b/types/lib/model.d.ts @@ -7,7 +7,7 @@ import { ValidationOptions } from './instance-validator'; import { IndexesOptions, QueryOptions, TableName } from './query-interface'; import { Sequelize, SyncOptions } from './sequelize'; import { LOCK, Transaction } from './transaction'; -import { Col, Fn, Literal, Where } from './utils'; +import { Col, Literal, Where } from './utils'; import { SetRequired } from '../type-helpers/set-required' import Op from '../../lib/operators'; @@ -2958,9 +2958,11 @@ export default Model; */ export type AttributesOf = { [Key in keyof M as - M[Key] extends Fn ? never + M[Key] extends AnyFunction ? never : Key extends keyof Model ? never : Key extends Excluded ? never : Key ]: M[Key] }; + +type AnyFunction = (...args: any[]) => any; diff --git a/types/test/typescriptDocs/ModelInitWithAttributesOf.ts b/types/test/typescriptDocs/ModelInitWithAttributesOf.ts new file mode 100644 index 000000000000..14be8b2aed17 --- /dev/null +++ b/types/test/typescriptDocs/ModelInitWithAttributesOf.ts @@ -0,0 +1,197 @@ +/** + * Keep this file in sync with the code in the "Using with `AttributesOf`" section in typescript.md + */ +import { + Association, DataTypes, HasManyAddAssociationMixin, HasManyCountAssociationsMixin, + HasManyCreateAssociationMixin, HasManyGetAssociationsMixin, HasManyHasAssociationMixin, Model, + ModelDefined, Optional, Sequelize, AttributesOf +} from 'sequelize'; + +const sequelize = new Sequelize('mysql://root:asd123@localhost:3306/mydb'); + +// 'projects' is excluded as it's not an attribute, it's an association. +class User extends Model> { + // can be undefined during creation when using `autoIncrement` + declare id?: number; + declare name: string; + declare preferredName: string | null; // for nullable fields + + // timestamps! + // can be undefined during creation + declare readonly createdAt?: Date; + // can be undefined during creation + declare readonly updatedAt?: 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; // Note the null assertions! + declare addProject: HasManyAddAssociationMixin; + declare hasProject: HasManyHasAssociationMixin; + declare countProjects: HasManyCountAssociationsMixin; + declare createProject: HasManyCreateAssociationMixin; + + // You can also pre-declare possible inclusions, these will only be populated if you + // actively include a relation. + declare readonly projects?: Project[]; // Note this is optional since it's only populated when explicitly requested in code + + declare static associations: { + projects: Association; + }; +} + +// You can write `extends Model, AttributesOf>` instead, +// but that will do the exact same thing as below +class Project extends Model> { + // id can be undefined during creation when using `autoIncrement` + declare id?: number; + declare ownerId: number; + declare name: string; + + // can be undefined during creation + declare readonly createdAt?: Date; + // can be undefined during creation + declare readonly updatedAt?: Date; +} + +class Address extends Model> { + declare userId: number; + declare address: string; + + // can be undefined during creation + declare readonly createdAt?: Date; + // can be undefined during creation + declare readonly updatedAt?: Date; +} + +// You can also define modules in a functional way +interface NoteAttributes { + id: number; + title: string; + content: string; +} + +// You can also set multiple attributes optional at once +interface NoteCreationAttributes + extends Optional { +} + +Project.init( + { + id: { + type: DataTypes.INTEGER.UNSIGNED, + autoIncrement: true, + primaryKey: true + }, + ownerId: { + type: DataTypes.INTEGER.UNSIGNED, + allowNull: false + }, + name: { + type: new DataTypes.STRING(128), + allowNull: false + }, + }, + { + 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 + } + }, + { + tableName: 'users', + sequelize // passing the `sequelize` instance is required + } +); + +Address.init( + { + userId: { + type: DataTypes.INTEGER.UNSIGNED + }, + address: { + type: new DataTypes.STRING(128), + allowNull: false + } + }, + { + tableName: 'address', + sequelize // passing the `sequelize` instance is required + } +); + +// And with a functional approach defining a module looks like this +const Note: ModelDefined = sequelize.define( + 'Note', + { + id: { + type: DataTypes.INTEGER.UNSIGNED, + autoIncrement: true, + primaryKey: true + }, + title: { + type: new DataTypes.STRING(64), + defaultValue: 'Unnamed Note' + }, + content: { + type: new DataTypes.STRING(4096), + allowNull: false + } + }, + { + tableName: 'notes' + } +); + +// Here we associate which actually populates out pre-declared `association` static and other methods. +User.hasMany(Project, { + sourceKey: 'id', + foreignKey: 'ownerId', + as: 'projects' // this determines the name in `associations`! +}); + +Address.belongsTo(User, { targetKey: 'id' }); +User.hasOne(Address, { sourceKey: 'id' }); + +async function doStuffWithUser() { + const newUser = await User.create({ + name: 'Johnny', + preferredName: 'John', + }); + console.log(newUser.id, newUser.name, newUser.preferredName); + + const project = await newUser.createProject({ + name: 'first!' + }); + + const ourUser = await User.findByPk(1, { + include: [User.associations.projects], + rejectOnEmpty: true // Specifying true here removes `null` from the return type! + }); + + // Note the `!` null assertion since TS can't know if we included + // the model or not + console.log(ourUser.projects![0].name); +} + +(async () => { + await sequelize.sync(); + await doStuffWithUser(); +})(); From f895c87098b96e52348389192de32fddae5e961c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zo=C3=A9?= Date: Wed, 5 Jan 2022 14:54:53 +0100 Subject: [PATCH 03/21] docs: make comments more explicit Co-authored-by: Rik Smale <13023439+WikiRik@users.noreply.github.com> --- docs/manual/other-topics/typescript.md | 14 +++++++------- .../typescriptDocs/ModelInitWithAttributesOf.ts | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/manual/other-topics/typescript.md b/docs/manual/other-topics/typescript.md index bac894c2e22e..35669a574009 100644 --- a/docs/manual/other-topics/typescript.md +++ b/docs/manual/other-topics/typescript.md @@ -329,15 +329,15 @@ const sequelize = new Sequelize('mysql://root:asd123@localhost:3306/mydb'); // 'projects' is excluded as it's not an attribute, it's an association. class User extends Model> { - // can be undefined during creation when using `autoIncrement` + // id can be undefined during creation when using `autoIncrement` declare id?: number; declare name: string; declare preferredName: string | null; // for nullable fields // timestamps! - // can be undefined during creation + // createdAt can be undefined during creation declare readonly createdAt?: Date; - // can be undefined during creation + // updatedAt can be undefined during creation declare readonly updatedAt?: Date; // Since TS cannot determine model association at compile time @@ -366,9 +366,9 @@ class Project extends Model> { declare ownerId: number; declare name: string; - // can be undefined during creation + // createdAt can be undefined during creation declare readonly createdAt?: Date; - // can be undefined during creation + // updatedAt can be undefined during creation declare readonly updatedAt?: Date; } @@ -376,9 +376,9 @@ class Address extends Model> { declare userId: number; declare address: string; - // can be undefined during creation + // createdAt can be undefined during creation declare readonly createdAt?: Date; - // can be undefined during creation + // updatedAt can be undefined during creation declare readonly updatedAt?: Date; } diff --git a/types/test/typescriptDocs/ModelInitWithAttributesOf.ts b/types/test/typescriptDocs/ModelInitWithAttributesOf.ts index 14be8b2aed17..681779e6135f 100644 --- a/types/test/typescriptDocs/ModelInitWithAttributesOf.ts +++ b/types/test/typescriptDocs/ModelInitWithAttributesOf.ts @@ -11,15 +11,15 @@ const sequelize = new Sequelize('mysql://root:asd123@localhost:3306/mydb'); // 'projects' is excluded as it's not an attribute, it's an association. class User extends Model> { - // can be undefined during creation when using `autoIncrement` + // id can be undefined during creation when using `autoIncrement` declare id?: number; declare name: string; declare preferredName: string | null; // for nullable fields // timestamps! - // can be undefined during creation + // createdAt can be undefined during creation declare readonly createdAt?: Date; - // can be undefined during creation + // updatedAt can be undefined during creation declare readonly updatedAt?: Date; // Since TS cannot determine model association at compile time @@ -48,9 +48,9 @@ class Project extends Model> { declare ownerId: number; declare name: string; - // can be undefined during creation + // createdAt can be undefined during creation declare readonly createdAt?: Date; - // can be undefined during creation + // updatedAt can be undefined during creation declare readonly updatedAt?: Date; } @@ -58,9 +58,9 @@ class Address extends Model> { declare userId: number; declare address: string; - // can be undefined during creation + // createdAt can be undefined during creation declare readonly createdAt?: Date; - // can be undefined during creation + // updatedAt can be undefined during creation declare readonly updatedAt?: Date; } From 271e39221012c663e0a4cc3181c917d6dcd2c9dc Mon Sep 17 00:00:00 2001 From: Guylian Cox Date: Wed, 5 Jan 2022 15:00:39 +0100 Subject: [PATCH 04/21] docs: hide maintainer-only information in typescript.md --- docs/manual/other-topics/typescript.md | 26 +++++++------------ types/test/typescriptDocs/Define.ts | 4 ++- .../test/typescriptDocs/DefineNoAttributes.ts | 4 ++- types/test/typescriptDocs/ModelInit.ts | 5 +++- .../typescriptDocs/ModelInitNoAttributes.ts | 4 ++- .../ModelInitWithAttributesOf.ts | 5 +++- 6 files changed, 27 insertions(+), 21 deletions(-) diff --git a/docs/manual/other-topics/typescript.md b/docs/manual/other-topics/typescript.md index 35669a574009..186b5bd9e1a7 100644 --- a/docs/manual/other-topics/typescript.md +++ b/docs/manual/other-topics/typescript.md @@ -18,12 +18,9 @@ Example of a minimal TypeScript project with strict type-checking for attributes **Important**: You must use `declare` on your class properties typings to ensure TypeScript does not emit those class properties. See [Caveat with Public Class Fields](./model-basics.html#caveat-with-public-class-fields) -**NOTE:** Keep the following code in sync with `/types/test/typescriptDocs/ModelInit.ts` to ensure it typechecks correctly. + ```typescript -/** - * Keep this file in sync with the code in the "Usage" section in typescript.md - */ import { Association, DataTypes, HasManyAddAssociationMixin, HasManyCountAssociationsMixin, HasManyCreateAssociationMixin, HasManyGetAssociationsMixin, HasManyHasAssociationMixin, Model, @@ -247,8 +244,8 @@ async function doStuffWithUser() { The typings for Sequelize v5 allowed you to define models without specifying types for the attributes. This is still possible for backwards compatibility and for cases where you feel strict typing for attributes isn't worth it. -**NOTE:** Keep the following code in sync with `typescriptDocs/ModelInitNoAttributes.ts` to ensure -it typechecks correctly. + ```ts import { Sequelize, Model, DataTypes } from "sequelize"; @@ -301,8 +298,6 @@ async function doStuffWithUserModel() { `AttributeOf` is a utility type you can use to automatically extract attributes from your class definition. It's an alternative solution designed to reduce the boilerplate of the above solution. -Here is the same example as above, but using `AttributeOf`. - Some caveats of this solution: - it's not possible to use `AttributesOf` for `CreationAttributes`. @@ -313,12 +308,11 @@ Some caveats of this solution: If one of your attributes shares the same name as one of the properties of `Model`, change its name. Doing this is likely to cause issues anyway. -**NOTE:** Keep the following code in sync with `/types/test/typescriptDocs/ModelInitWithAttributesOf.ts` to ensure it typechecks correctly. +Here is the same example as above, but using `AttributeOf`. + + ```typescript -/** - * Keep this file in sync with the code in the "Using with `AttributesOf`" section in typescript.md - */ import { Association, DataTypes, HasManyAddAssociationMixin, HasManyCountAssociationsMixin, HasManyCreateAssociationMixin, HasManyGetAssociationsMixin, HasManyHasAssociationMixin, Model, @@ -519,8 +513,8 @@ async function doStuffWithUser() { In Sequelize versions before v5, the default way of defining a model involved using `sequelize.define`. It's still possible to define models with that, and you can also add typings to these models using interfaces. -**NOTE:** Keep the following code in sync with `typescriptDocs/Define.ts` to ensure -it typechecks correctly. + ```ts import { Sequelize, Model, DataTypes, Optional } from "sequelize"; @@ -561,8 +555,8 @@ async function doStuff() { If you're comfortable with somewhat less strict typing for the attributes on a model, you can save some code by defining the Instance to just extend `Model` without any attributes in the generic types. -**NOTE:** Keep the following code in sync with `typescriptDocs/DefineNoAttributes.ts` to ensure -it typechecks correctly. + ```ts import { Sequelize, Model, DataTypes } from "sequelize"; diff --git a/types/test/typescriptDocs/Define.ts b/types/test/typescriptDocs/Define.ts index 9e222311053d..8ea23694d01c 100644 --- a/types/test/typescriptDocs/Define.ts +++ b/types/test/typescriptDocs/Define.ts @@ -1,6 +1,8 @@ /** * Keep this file in sync with the code in the "Usage of `sequelize.define`" - * section in typescript.md + * section in /docs/manual/other-topics/typescript.md + * + * Don't include this comment in the md file. */ import { Sequelize, Model, DataTypes, Optional } from 'sequelize'; diff --git a/types/test/typescriptDocs/DefineNoAttributes.ts b/types/test/typescriptDocs/DefineNoAttributes.ts index ac2d70069a13..8273cad18ff1 100644 --- a/types/test/typescriptDocs/DefineNoAttributes.ts +++ b/types/test/typescriptDocs/DefineNoAttributes.ts @@ -1,6 +1,8 @@ /** * Keep this file in sync with the code in the "Usage of `sequelize.define`" - * that doesn't have attribute types in typescript.md + * that doesn't have attribute types in /docs/manual/other-topics/typescript.md + * + * Don't include this comment in the md file. */ import { Sequelize, Model, DataTypes } from 'sequelize'; diff --git a/types/test/typescriptDocs/ModelInit.ts b/types/test/typescriptDocs/ModelInit.ts index d0720fe2628f..88e48c311345 100644 --- a/types/test/typescriptDocs/ModelInit.ts +++ b/types/test/typescriptDocs/ModelInit.ts @@ -1,5 +1,8 @@ /** - * Keep this file in sync with the code in the "Usage" section in typescript.md + * Keep this file in sync with the code in the "Usage" section + * in /docs/manual/other-topics/typescript.md + * + * Don't include this comment in the md file. */ import { Association, DataTypes, HasManyAddAssociationMixin, HasManyCountAssociationsMixin, diff --git a/types/test/typescriptDocs/ModelInitNoAttributes.ts b/types/test/typescriptDocs/ModelInitNoAttributes.ts index b53ea86fc12a..ff8a5c22a22d 100644 --- a/types/test/typescriptDocs/ModelInitNoAttributes.ts +++ b/types/test/typescriptDocs/ModelInitNoAttributes.ts @@ -1,6 +1,8 @@ /** * Keep this file in sync with the code in the "Usage without strict types for - * attributes" section in typescript.md + * attributes" section in /docs/manual/other-topics/typescript.md + * + * Don't include this comment in the md file. */ import { Sequelize, Model, DataTypes } from 'sequelize'; diff --git a/types/test/typescriptDocs/ModelInitWithAttributesOf.ts b/types/test/typescriptDocs/ModelInitWithAttributesOf.ts index 681779e6135f..efe4d0bf7dc3 100644 --- a/types/test/typescriptDocs/ModelInitWithAttributesOf.ts +++ b/types/test/typescriptDocs/ModelInitWithAttributesOf.ts @@ -1,5 +1,8 @@ /** - * Keep this file in sync with the code in the "Using with `AttributesOf`" section in typescript.md + * Keep this file in sync with the code in the "Using with `AttributesOf`" section + * in /docs/manual/other-topics/typescript.md + * + * Don't include this comment in the md file. */ import { Association, DataTypes, HasManyAddAssociationMixin, HasManyCountAssociationsMixin, From 3f833fc52efb469ae463b6934a4590fd4f0a376c Mon Sep 17 00:00:00 2001 From: Guylian Cox Date: Wed, 5 Jan 2022 15:01:26 +0100 Subject: [PATCH 05/21] fix(types): revert accidental removal of Fn import --- types/lib/model.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/lib/model.d.ts b/types/lib/model.d.ts index c03801c2144a..ffac53aefc63 100644 --- a/types/lib/model.d.ts +++ b/types/lib/model.d.ts @@ -7,7 +7,7 @@ import { ValidationOptions } from './instance-validator'; import { IndexesOptions, QueryOptions, TableName } from './query-interface'; import { Sequelize, SyncOptions } from './sequelize'; import { LOCK, Transaction } from './transaction'; -import { Col, Literal, Where } from './utils'; +import { Col, Fn, Literal, Where } from './utils'; import { SetRequired } from '../type-helpers/set-required' import Op from '../../lib/operators'; From 75f17c433d11460d59435d3de1eb2dd235fc3a10 Mon Sep 17 00:00:00 2001 From: Guylian Cox Date: Fri, 7 Jan 2022 13:50:32 +0100 Subject: [PATCH 06/21] feat(types): add `CreationAttributesOf` works like `AttributesOf` but fields tagged as `CreationOptional` will accept undefined --- types/lib/model.d.ts | 86 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/types/lib/model.d.ts b/types/lib/model.d.ts index ffac53aefc63..671dc7e0d4dd 100644 --- a/types/lib/model.d.ts +++ b/types/lib/model.d.ts @@ -1,4 +1,4 @@ -import { IndexHints } from '..'; +import { IndexHints, Optional } from '..'; import { Association, BelongsTo, BelongsToMany, BelongsToManyOptions, BelongsToOptions, HasMany, HasManyOptions, HasOne, HasOneOptions } from './associations/index'; import { DataType } from './data-types'; import { Deferrable } from './deferrable'; @@ -11,6 +11,47 @@ import { Col, Fn, Literal, Where } from './utils'; import { SetRequired } from '../type-helpers/set-required' import Op from '../../lib/operators'; +type AnyFunction = (...args: any[]) => any; + +/** + * Returns all shallow properties that accept `undefined`. + * Does not include Optional properties, only `undefined`. + * + * @example + * type UndefinedProps = UndefinedPropertiesOf<{ + * id: number | undefined, + * createdAt: string | undefined, + * firstName: string, + * lastName?: string, // optional properties are not included. + * }>; + * + * // is equal to + * + * type UndefinedProps = 'id' | 'createdAt'; + */ +type UndefinedPropertiesOf = { + [P in keyof T]-?: undefined extends T[P] ? P : never +}[keyof T]; + +/** + * Makes all shallow properties of an object `optional` if they accept `undefined` as a value. + * + * @example + * type MyOptionalType = OptionalUndefined<{ + * id: number | undefined, + * name: string, + * }>; + * + * // is equal to + * + * type MyOptionalType = { + * // this property is optional. + * id?: number | undefined, + * name: string, + * }; + */ +type OptionalUndefined = Optional>; + export interface Logging { /** * A function that gets executed while running the query to log the sql. @@ -2965,4 +3006,45 @@ export type AttributesOf = { ]: M[Key] }; -type AnyFunction = (...args: any[]) => any; +/** + * Dummy Symbol used as branding by {@link CreationOptional}. + * + * Do not export, Do not use. + */ +declare const CreationAttributeBrand: unique symbol; + +/** + * This is a Branded Type. + * You can use it to tag attributes that can be ommited during Model Creation. + * + * For use with {@link CreationAttributesOf}. + * + * @see CreationAttributesOf + */ +export type CreationOptional = T & { [CreationAttributeBrand]: true }; + +/** + * Utility type to extract Attributes of a given Model. + * + * Works like {@link AttributesOf}, but fields that are tagged using + * {@link CreationOptional} will be optional. + * + * @example + * class User extends Model, CreationAttributesOf> { + * // this attribute is optional in Model#create + * declare id: CreationOptional; + * + * // this attribute is mandatory in Model#create + * declare name: string; + * } + */ +export type CreationAttributesOf = { + [Key in keyof M as + Key extends `_${string}` ? never + : M[Key] extends Fn ? never + : Key extends keyof Model ? never + : Key extends Excluded ? never + : Key + ]: M[Key] extends CreationOptional ? (Val | undefined) + : M[Key] +}; From a55c39fc700627161781992da244ac0c8d8d2b75 Mon Sep 17 00:00:00 2001 From: Guylian Cox Date: Fri, 7 Jan 2022 14:04:34 +0100 Subject: [PATCH 07/21] fix(types): make `undefined` fields optional in CreationAttributes --- types/lib/model.d.ts | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/types/lib/model.d.ts b/types/lib/model.d.ts index 671dc7e0d4dd..0e37fc0fd4d9 100644 --- a/types/lib/model.d.ts +++ b/types/lib/model.d.ts @@ -37,7 +37,7 @@ type UndefinedPropertiesOf = { * Makes all shallow properties of an object `optional` if they accept `undefined` as a value. * * @example - * type MyOptionalType = OptionalUndefined<{ + * type MyOptionalType = MakeUndefinedOptional<{ * id: number | undefined, * name: string, * }>; @@ -50,7 +50,7 @@ type UndefinedPropertiesOf = { * name: string, * }; */ -type OptionalUndefined = Optional>; +type MakeUndefinedOptional = Optional>; export interface Logging { /** @@ -2036,7 +2036,7 @@ export abstract class Model( this: ModelStatic, - record?: M['_creationAttributes'], + record?: MakeUndefinedOptional, options?: BuildOptions ): M; @@ -2045,7 +2045,7 @@ export abstract class Model( this: ModelStatic, - records: ReadonlyArray, + records: ReadonlyArray>, options?: BuildOptions ): M[]; @@ -2057,7 +2057,7 @@ export abstract class Model = CreateOptions >( this: ModelStatic, - values?: M['_creationAttributes'], + values?: MakeUndefinedOptional, options?: O ): Promise; @@ -2067,7 +2067,10 @@ export abstract class Model( this: ModelStatic, - options: FindOrCreateOptions + options: FindOrCreateOptions< + M['_attributes'], + MakeUndefinedOptional + > ): Promise<[M, boolean]>; /** @@ -2083,7 +2086,7 @@ export abstract class Model( this: ModelStatic, - options: FindOrCreateOptions + options: FindOrCreateOptions> ): Promise<[M, boolean]>; /** @@ -2092,7 +2095,7 @@ export abstract class Model( this: ModelStatic, - options: FindOrCreateOptions + options: FindOrCreateOptions> ): Promise<[M, boolean]>; /** @@ -2116,7 +2119,7 @@ export abstract class Model( this: ModelStatic, - values: M['_creationAttributes'], + values: MakeUndefinedOptional, options?: UpsertOptions ): Promise<[M, boolean | null]>; @@ -2133,7 +2136,7 @@ export abstract class Model( this: ModelStatic, - records: ReadonlyArray, + records: ReadonlyArray>, options?: BulkCreateOptions ): Promise; @@ -2746,7 +2749,7 @@ export abstract class Model, options?: BuildOptions); /** * Get an object representing the query for this instance, use with `options.where` @@ -2821,8 +2824,8 @@ export abstract class Model; - public previous(key: K): TCreationAttributes[K] | undefined; + public previous(): Partial; + public previous(key: K): TModelAttributes[K] | undefined; /** * Validates this instance, and if the validation passes, persists it to the database. @@ -3041,7 +3044,7 @@ export type CreationOptional = T & { [CreationAttributeBrand]: true }; export type CreationAttributesOf = { [Key in keyof M as Key extends `_${string}` ? never - : M[Key] extends Fn ? never + : M[Key] extends AnyFunction ? never : Key extends keyof Model ? never : Key extends Excluded ? never : Key From 932e6187e112e217810599a6c4e7ae027a92c0ec Mon Sep 17 00:00:00 2001 From: Guylian Cox Date: Fri, 7 Jan 2022 15:31:14 +0100 Subject: [PATCH 08/21] feat(types): merge (Creation)AttributesOf, drop branded type --- types/lib/associations/has-many.d.ts | 3 +- types/lib/model.d.ts | 119 +++-------- types/lib/utils.d.ts | 42 ++++ types/test/attributes-of.ts | 72 +++++++ types/test/attributes.ts | 24 +-- types/test/create.ts | 4 +- types/test/models/User.ts | 106 +++++----- types/test/models/UserGroup.ts | 72 ++++--- types/test/typescriptDocs/ModelInit.ts | 157 +++++++------- .../ModelInitWithAttributesOf.ts | 200 ------------------ 10 files changed, 332 insertions(+), 467 deletions(-) create mode 100644 types/test/attributes-of.ts delete mode 100644 types/test/typescriptDocs/ModelInitWithAttributesOf.ts diff --git a/types/lib/associations/has-many.d.ts b/types/lib/associations/has-many.d.ts index b1780aa60b4a..248caa7c988d 100644 --- a/types/lib/associations/has-many.d.ts +++ b/types/lib/associations/has-many.d.ts @@ -9,6 +9,7 @@ import { Transactionable } from '../model'; import { Association, ManyToManyOptions, MultiAssociationAccessors } from './base'; +import { MakeUndefinedOptional } from '../utils'; /** * Options provided when associating models with hasMany relationship @@ -214,7 +215,7 @@ export type HasManyCreateAssociationMixin< TForeignKey extends keyof TModel['_creationAttributes'] = never, TScope extends keyof TModel['_creationAttributes'] = never > = ( - values?: Omit, + values?: Omit, TForeignKey | TScope>, options?: HasManyCreateAssociationMixinOptions ) => Promise; diff --git a/types/lib/model.d.ts b/types/lib/model.d.ts index 0e37fc0fd4d9..37d0c3e6ed66 100644 --- a/types/lib/model.d.ts +++ b/types/lib/model.d.ts @@ -1,4 +1,4 @@ -import { IndexHints, Optional } from '..'; +import { IndexHints } from '..'; import { Association, BelongsTo, BelongsToMany, BelongsToManyOptions, BelongsToOptions, HasMany, HasManyOptions, HasOne, HasOneOptions } from './associations/index'; import { DataType } from './data-types'; import { Deferrable } from './deferrable'; @@ -7,51 +7,10 @@ import { ValidationOptions } from './instance-validator'; import { IndexesOptions, QueryOptions, TableName } from './query-interface'; import { Sequelize, SyncOptions } from './sequelize'; import { LOCK, Transaction } from './transaction'; -import { Col, Fn, Literal, Where } from './utils'; +import { Col, Fn, Literal, Where, MakeUndefinedOptional, AnyFunction } from './utils'; import { SetRequired } from '../type-helpers/set-required' import Op from '../../lib/operators'; -type AnyFunction = (...args: any[]) => any; - -/** - * Returns all shallow properties that accept `undefined`. - * Does not include Optional properties, only `undefined`. - * - * @example - * type UndefinedProps = UndefinedPropertiesOf<{ - * id: number | undefined, - * createdAt: string | undefined, - * firstName: string, - * lastName?: string, // optional properties are not included. - * }>; - * - * // is equal to - * - * type UndefinedProps = 'id' | 'createdAt'; - */ -type UndefinedPropertiesOf = { - [P in keyof T]-?: undefined extends T[P] ? P : never -}[keyof T]; - -/** - * Makes all shallow properties of an object `optional` if they accept `undefined` as a value. - * - * @example - * type MyOptionalType = MakeUndefinedOptional<{ - * id: number | undefined, - * name: string, - * }>; - * - * // is equal to - * - * type MyOptionalType = { - * // this property is optional. - * id?: number | undefined, - * name: string, - * }; - */ -type MakeUndefinedOptional = Optional>; - export interface Logging { /** * A function that gets executed while running the query to log the sql. @@ -2971,6 +2930,20 @@ export type ModelStatic = NonConstructor & { new( export default Model; +/** + * Option bag for {@link AttributesOf}. + * + * - omit: properties to not treat as Attributes. + * - optional: properties that are optional. For use with CreationAttributes. + */ +type AttributesOfOptions< + Excluded, + Optional +> = { + omit?: Excluded, + optional?: Optional, +}; + /** * Utility type to extract Attributes of a given Model. * @@ -2991,63 +2964,35 @@ export default Model; * * @example * // listed attributes will be 'id' & 'firstName'. - * // we're excluding the `name` & `test` getters using the second argument. - * class User extends Model> { + * // we're excluding the `name` & `test` getters using the `omit` option. + * class User extends Model> { * id: number; * firstName: string; * * get name() { return this.firstName; } * get test() { return ''; } * } - */ -export type AttributesOf = { - [Key in keyof M as - M[Key] extends AnyFunction ? never - : Key extends keyof Model ? never - : Key extends Excluded ? never - : Key - ]: M[Key] -}; - -/** - * Dummy Symbol used as branding by {@link CreationOptional}. - * - * Do not export, Do not use. - */ -declare const CreationAttributeBrand: unique symbol; - -/** - * This is a Branded Type. - * You can use it to tag attributes that can be ommited during Model Creation. - * - * For use with {@link CreationAttributesOf}. - * - * @see CreationAttributesOf - */ -export type CreationOptional = T & { [CreationAttributeBrand]: true }; - -/** - * Utility type to extract Attributes of a given Model. - * - * Works like {@link AttributesOf}, but fields that are tagged using - * {@link CreationOptional} will be optional. * * @example - * class User extends Model, CreationAttributesOf> { - * // this attribute is optional in Model#create - * declare id: CreationOptional; + * // You can mark some properties as Optional during creation using the `optional` option. * - * // this attribute is mandatory in Model#create - * declare name: string; + * class User extends Model, AttributesOf> { + * // this attribute will be optional in User.create + * id: number; + * firstName: string; * } */ -export type CreationAttributesOf = { +export type AttributesOf< + M extends Model, + Options extends AttributesOfOptions = { omit: never, optional: never } + > = { [Key in keyof M as - Key extends `_${string}` ? never - : M[Key] extends AnyFunction ? never + M[Key] extends AnyFunction ? never : Key extends keyof Model ? never - : Key extends Excluded ? never + // check 'omit' option is provided + : Options['omit'] extends string ? (Key extends Options['omit'] ? never : Key) : Key - ]: M[Key] extends CreationOptional ? (Val | undefined) + // check 'optional' option is provided + ]: Options['optional'] extends string ? (Key extends Options['optional'] ? (M[Key] | undefined) : M[Key]) : M[Key] }; diff --git a/types/lib/utils.d.ts b/types/lib/utils.d.ts index 02552a46c1f8..89007e46f1a2 100644 --- a/types/lib/utils.d.ts +++ b/types/lib/utils.d.ts @@ -1,5 +1,6 @@ import { DataType } from './data-types'; import { Model, ModelCtor, ModelType, WhereOptions } from './model'; +import { Optional } from '..'; export type Primitive = 'string' | 'number' | 'boolean'; @@ -122,3 +123,44 @@ export class Where extends SequelizeMethod { constructor(attr: object, comparator: string, logic: string | object); constructor(attr: object, logic: string | object); } + +export type AnyFunction = (...args: any[]) => any; + +/** + * Returns all shallow properties that accept `undefined`. + * Does not include Optional properties, only `undefined`. + * + * @example + * type UndefinedProps = UndefinedPropertiesOf<{ + * id: number | undefined, + * createdAt: string | undefined, + * firstName: string, + * lastName?: string, // optional properties are not included. + * }>; + * + * // is equal to + * + * type UndefinedProps = 'id' | 'createdAt'; + */ +export type UndefinedPropertiesOf = { + [P in keyof T]-?: undefined extends T[P] ? P : never +}[keyof T]; + +/** + * Makes all shallow properties of an object `optional` if they accept `undefined` as a value. + * + * @example + * type MyOptionalType = MakeUndefinedOptional<{ + * id: number | undefined, + * name: string, + * }>; + * + * // is equal to + * + * type MyOptionalType = { + * // this property is optional. + * id?: number | undefined, + * name: string, + * }; + */ +export type MakeUndefinedOptional = Optional>; diff --git a/types/test/attributes-of.ts b/types/test/attributes-of.ts new file mode 100644 index 000000000000..887536d9701c --- /dev/null +++ b/types/test/attributes-of.ts @@ -0,0 +1,72 @@ +import { expectTypeOf } from 'expect-type'; +import { AttributesOf, Model } from 'sequelize'; + +type GroupAttributes = AttributesOf; + +class Group extends Model { + declare id: number; +} + +{ + // @ts-expect-error - id should not be missing + const fail1: GroupAttributes = {}; + + // @ts-expect-error - id should not be missing + const fail2: AttributesOf = {}; + + // @ts-expect-error - id should not be missing + const fail3: AttributesOf = {}; +} + +type UserAttributes = AttributesOf; +type UserCreationAttributes = AttributesOf; + +class User extends Model { + declare id: number; + declare name: string | undefined; + + declare groups: Group[]; + + instanceMethod() {} + static staticMethod() {} +} + +expectTypeOf().not.toBeAny(); + +{ + const win: UserAttributes = { + id: 1, + name: '', + }; + + const fail1: UserAttributes = { + id: 1, + name: '', + // @ts-expect-error - 'extra' should not be present + extra: '' + }; + +// @ts-expect-error - 'name' should be present + const fail2: UserAttributes = { + id: 1, + }; +} + +{ + const win: UserCreationAttributes = { + id: undefined, + name: '', + }; + + const fail1: UserCreationAttributes = { + id: 1, + name: '', + // @ts-expect-error 'extra' does not exist + extra: '' + }; + +// @ts-expect-error missing 'name' + const fail2: UserCreationAttributes = { + id: 1, + }; +} diff --git a/types/test/attributes.ts b/types/test/attributes.ts index 2c754923debd..c01ab6274af8 100644 --- a/types/test/attributes.ts +++ b/types/test/attributes.ts @@ -8,14 +8,12 @@ interface UserAttributes extends UserCreationAttributes { id: number; } -class User - extends Model - implements UserAttributes { - public id!: number; - public name!: string; - - public readonly projects?: Project[]; - public readonly address?: Address; +class User extends Model implements UserAttributes { + declare id: number; + declare name: string; + + declare readonly projects?: Project[]; + declare readonly address?: Address; } interface ProjectCreationAttributes { @@ -30,14 +28,14 @@ interface ProjectAttributes extends ProjectCreationAttributes { class Project extends Model implements ProjectAttributes { - public id!: number; - public ownerId!: number; - public name!: string; + declare id: number; + declare ownerId: number; + declare name: string; } class Address extends Model { - public userId!: number; - public address!: string; + declare userId: number; + declare address: string; } // both models should be accepted in include diff --git a/types/test/create.ts b/types/test/create.ts index a370249b47f6..ab58c11fd416 100644 --- a/types/test/create.ts +++ b/types/test/create.ts @@ -1,7 +1,7 @@ import { expectTypeOf } from 'expect-type' import { User } from './models/User'; -async () => { +(async () => { const user = await User.create({ id: 123, firstName: '', @@ -71,4 +71,4 @@ async () => { // @ts-expect-error unknown attribute unknown: '', }); -}; +})(); diff --git a/types/test/models/User.ts b/types/test/models/User.ts index b1a3d23abf90..10a7d0482885 100644 --- a/types/test/models/User.ts +++ b/types/test/models/User.ts @@ -1,4 +1,5 @@ import { + AttributesOf, BelongsTo, BelongsToCreateAssociationMixin, BelongsToGetAssociationMixin, @@ -6,88 +7,85 @@ import { DataTypes, FindOptions, Model, - ModelCtor, + ModelStatic, Op, - Optional } from 'sequelize'; import { sequelize } from '../connection'; -export interface UserAttributes { - id: number; - username: string; - firstName: string; - lastName: string; - groupId: number; -} - -/** - * In this case, we make most fields optional. In real cases, - * only fields that have default/autoincrement values should be made optional. - */ -export interface UserCreationAttributes extends Optional {} +type NonUserAttributes = 'group'; +type CreationOptionalAttrs = 'id' | 'createdAt' | 'updatedAt' | 'username' | 'lastName' | 'groupId'; -export class User extends Model implements UserAttributes { +export class User extends Model< + AttributesOf, + AttributesOf +> { public static associations: { group: BelongsTo; }; - public id!: number; - public username!: string; - public firstName!: string; - public lastName!: string; - public groupId!: number; - public createdAt!: Date; - public updatedAt!: Date; + declare id: number; + declare createdAt: Date; + declare updatedAt: Date; + + declare username: string | null; + declare firstName: string; + declare lastName: string | null; + declare groupId: number | null; // mixins for association (optional) - public group?: UserGroup; - public getGroup!: BelongsToGetAssociationMixin; - public setGroup!: BelongsToSetAssociationMixin; - public createGroup!: BelongsToCreateAssociationMixin; + declare group?: UserGroup; + declare getGroup: BelongsToGetAssociationMixin; + declare setGroup: BelongsToSetAssociationMixin; + declare createGroup: BelongsToCreateAssociationMixin; } User.init( { id: { type: DataTypes.NUMBER, - primaryKey: true, + primaryKey: true + }, + firstName: { + type: DataTypes.STRING, + allowNull: false, }, - firstName: DataTypes.STRING, lastName: DataTypes.STRING, username: DataTypes.STRING, groupId: DataTypes.NUMBER, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE, }, { version: true, getterMethods: { a() { return 1; - }, + } }, setterMethods: { b(val: string) { this.username = val; - }, + } }, scopes: { custom(a: number) { return { where: { - firstName: a, - }, + firstName: a + } }; }, custom2() { - return {} + return {}; } }, indexes: [{ fields: ['firstName'], using: 'BTREE', name: 'firstNameIdx', - concurrently: true, + concurrently: true }], - sequelize, + sequelize } ); @@ -96,9 +94,9 @@ User.afterSync(() => { fields: ['lastName'], using: 'BTREE', name: 'lastNameIdx', - concurrently: true, - }) -}) + concurrently: true + }); +}); // Hooks User.afterFind((users, options) => { @@ -106,7 +104,7 @@ User.afterFind((users, options) => { }); // TODO: VSCode shows the typing being correctly narrowed but doesn't do it correctly -User.addHook('beforeFind', 'test', (options: FindOptions) => { +User.addHook('beforeFind', 'test', (options: FindOptions>) => { return undefined; }); @@ -119,22 +117,22 @@ User.addHook('afterDestroy', async (instance, options) => { User.addScope('withoutFirstName', { where: { firstName: { - [Op.is]: null, - }, - }, + [Op.is]: null + } + } }); User.addScope( 'withFirstName', (firstName: string) => ({ - where: { firstName }, - }), + where: { firstName } + }) ); // associate // it is important to import _after_ the model above is already exported so the circular reference works. import { UserGroup } from './UserGroup'; -import { UserPost } from "./UserPost"; +import { UserPost } from './UserPost'; // associate with a class-based model export const Group = User.belongsTo(UserGroup, { as: 'group', foreignKey: 'groupId' }); @@ -143,21 +141,21 @@ User.hasMany(UserPost, { as: 'posts', foreignKey: 'userId' }); UserPost.belongsTo(User, { foreignKey: 'userId', targetKey: 'id', - as: 'user', + as: 'user' }); // associations refer to their Model -const userType: ModelCtor = User.associations.group.source; -const groupType: ModelCtor = User.associations.group.target; +const userType: ModelStatic = User.associations.group.source; +const groupType: ModelStatic = User.associations.group.target; // should associate correctly with both sequelize.define and class-based models -User.findOne({ include: [{ model: UserGroup }]}); -User.findOne({ include: [{ model: UserPost }]}); +User.findOne({ include: [{ model: UserGroup }] }); +User.findOne({ include: [{ model: UserPost }] }); User.scope([ 'custom2', - { method: [ 'custom', 32 ] } -]) + { method: ['custom', 32] } +]); const instance = new User({ username: 'foo', firstName: 'bar', lastName: 'baz' }); -instance.isSoftDeleted() +instance.isSoftDeleted(); diff --git a/types/test/models/UserGroup.ts b/types/test/models/UserGroup.ts index 31c385ab2044..2766709b51de 100644 --- a/types/test/models/UserGroup.ts +++ b/types/test/models/UserGroup.ts @@ -1,16 +1,17 @@ import { - DataTypes, - HasMany, - HasManyAddAssociationMixin, - HasManyAddAssociationsMixin, - HasManyCountAssociationsMixin, - HasManyCreateAssociationMixin, - HasManyGetAssociationsMixin, - HasManyHasAssociationMixin, - HasManyRemoveAssociationMixin, - HasManyRemoveAssociationsMixin, - HasManySetAssociationsMixin, - Model + AttributesOf, + DataTypes, + HasMany, + HasManyAddAssociationMixin, + HasManyAddAssociationsMixin, + HasManyCountAssociationsMixin, + HasManyCreateAssociationMixin, + HasManyGetAssociationsMixin, + HasManyHasAssociationMixin, + HasManyRemoveAssociationMixin, + HasManyRemoveAssociationsMixin, + HasManySetAssociationsMixin, + Model } from 'sequelize'; import { sequelize } from '../connection'; // associate @@ -19,29 +20,40 @@ import { User } from './User'; // This class doesn't extend the generic Model, but should still // function just fine, with a bit less safe type-checking -export class UserGroup extends Model { - public static associations: { - users: HasMany - }; +export class UserGroup extends Model< + AttributesOf, + AttributesOf +> { + public static associations: { + users: HasMany + }; - public id!: number; - public name!: string; + declare id: number; + declare name: string; - // mixins for association (optional) - public users!: User[]; - public getUsers!: HasManyGetAssociationsMixin; - public setUsers!: HasManySetAssociationsMixin; - public addUser!: HasManyAddAssociationMixin; - public addUsers!: HasManyAddAssociationsMixin; - public createUser!: HasManyCreateAssociationMixin; - public countUsers!: HasManyCountAssociationsMixin; - public hasUser!: HasManyHasAssociationMixin; - public removeUser!: HasManyRemoveAssociationMixin; - public removeUsers!: HasManyRemoveAssociationsMixin; + // mixins for association (optional) + declare users?: User[]; + declare getUsers: HasManyGetAssociationsMixin; + declare setUsers: HasManySetAssociationsMixin; + declare addUser: HasManyAddAssociationMixin; + declare addUsers: HasManyAddAssociationsMixin; + declare createUser: HasManyCreateAssociationMixin; + declare countUsers: HasManyCountAssociationsMixin; + declare hasUser: HasManyHasAssociationMixin; + declare removeUser: HasManyRemoveAssociationMixin; + declare removeUsers: HasManyRemoveAssociationsMixin; } // attach all the metadata to the model // instead of this, you could also use decorators -UserGroup.init({ name: DataTypes.STRING }, { sequelize }); +UserGroup.init({ + name: DataTypes.STRING, + id: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + autoIncrement: true + } +}, { sequelize }); export const Users = UserGroup.hasMany(User, { as: 'users', foreignKey: 'groupId' }); diff --git a/types/test/typescriptDocs/ModelInit.ts b/types/test/typescriptDocs/ModelInit.ts index 88e48c311345..e9ed9efd67e7 100644 --- a/types/test/typescriptDocs/ModelInit.ts +++ b/types/test/typescriptDocs/ModelInit.ts @@ -7,29 +7,27 @@ import { Association, DataTypes, HasManyAddAssociationMixin, HasManyCountAssociationsMixin, HasManyCreateAssociationMixin, HasManyGetAssociationsMixin, HasManyHasAssociationMixin, Model, - ModelDefined, Optional, Sequelize -} from "sequelize"; - -const sequelize = new Sequelize("mysql://root:asd123@localhost:3306/mydb"); - -// These are all the attributes in the User model -interface UserAttributes { - id: number; - name: string; - preferredName: string | null; -} - -// Some attributes are optional in `User.build` and `User.create` calls -interface UserCreationAttributes extends Optional {} - -class User extends Model - implements UserAttributes { - declare id: number; // Note that the `null assertion` `!` is required in strict mode. + ModelDefined, Optional, Sequelize, AttributesOf +} from 'sequelize'; + +const sequelize = new Sequelize('mysql://root:asd123@localhost:3306/mydb'); + +// 'projects' is excluded as it's not an attribute, it's an association. +class User extends Model< + // attributes + AttributesOf, + // creation attributes + AttributesOf +> { + // id can be undefined during creation when using `autoIncrement` + declare id: number; declare name: string; declare preferredName: string | null; // for nullable fields // timestamps! + // createdAt can be undefined during creation declare readonly createdAt: Date; + // updatedAt can be undefined during creation declare readonly updatedAt: Date; // Since TS cannot determine model association at compile time @@ -50,74 +48,59 @@ class User extends Model }; } -interface ProjectAttributes { - id: number; - ownerId: number; - name: string; - description?: string; -} - -interface ProjectCreationAttributes extends Optional {} - -class Project extends Model - implements ProjectAttributes { +// You can write `extends Model, AttributesOf>` instead, +// but that will do the exact same thing as below +class Project extends Model< + // attributes + AttributesOf, + // creation attributes + AttributesOf +> { + // id can be undefined during creation when using `autoIncrement` declare id: number; declare ownerId: number; declare name: string; + // createdAt can be undefined during creation declare readonly createdAt: Date; + // updatedAt can be undefined during creation declare readonly updatedAt: Date; } -interface AddressAttributes { - userId: number; - address: string; -} - -// You can write `extends Model` instead, -// but that will do the exact same thing as below -class Address extends Model implements AddressAttributes { +class Address extends Model< + AttributesOf
, + AttributesOf + > { declare userId: number; declare address: string; + // createdAt can be undefined during creation declare readonly createdAt: Date; + // updatedAt can be undefined during creation declare readonly updatedAt: Date; } -// You can also define modules in a functional way -interface NoteAttributes { - id: number; - title: string; - content: string; -} - -// You can also set multiple attributes optional at once -interface NoteCreationAttributes - extends Optional {} - Project.init( { id: { type: DataTypes.INTEGER.UNSIGNED, autoIncrement: true, - primaryKey: true, + primaryKey: true }, ownerId: { type: DataTypes.INTEGER.UNSIGNED, - allowNull: false, + allowNull: false }, name: { type: new DataTypes.STRING(128), - allowNull: false, - }, - description: { - type: new DataTypes.STRING(128), - allowNull: true, + allowNull: false }, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE, }, { sequelize, - tableName: "projects", + tableName: 'projects' } ); @@ -126,89 +109,103 @@ User.init( id: { type: DataTypes.INTEGER.UNSIGNED, autoIncrement: true, - primaryKey: true, + primaryKey: true }, name: { type: new DataTypes.STRING(128), - allowNull: false, + allowNull: false }, preferredName: { type: new DataTypes.STRING(128), - allowNull: true, + allowNull: true }, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE, }, { - tableName: "users", - sequelize, // passing the `sequelize` instance is required + tableName: 'users', + sequelize // passing the `sequelize` instance is required } ); Address.init( { userId: { - type: DataTypes.INTEGER.UNSIGNED, + type: DataTypes.INTEGER.UNSIGNED }, address: { type: new DataTypes.STRING(128), - allowNull: false, + allowNull: false }, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE, }, { - tableName: "address", - sequelize, // passing the `sequelize` instance is required + tableName: 'address', + sequelize // passing the `sequelize` instance is required } ); +// You can also define modules in a functional way +interface NoteAttributes { + id: number; + title: string; + content: string; +} + +// You can also set multiple attributes optional at once +type NoteCreationAttributes = Optional; + // And with a functional approach defining a module looks like this const Note: ModelDefined< NoteAttributes, NoteCreationAttributes > = sequelize.define( - "Note", + 'Note', { id: { type: DataTypes.INTEGER.UNSIGNED, autoIncrement: true, - primaryKey: true, + primaryKey: true }, title: { type: new DataTypes.STRING(64), - defaultValue: "Unnamed Note", + defaultValue: 'Unnamed Note' }, content: { type: new DataTypes.STRING(4096), - allowNull: false, - }, + allowNull: false + } }, { - tableName: "notes", + tableName: 'notes' } ); // Here we associate which actually populates out pre-declared `association` static and other methods. User.hasMany(Project, { - sourceKey: "id", - foreignKey: "ownerId", - as: "projects", // this determines the name in `associations`! + sourceKey: 'id', + foreignKey: 'ownerId', + as: 'projects' // this determines the name in `associations`! }); -Address.belongsTo(User, { targetKey: "id" }); -User.hasOne(Address, { sourceKey: "id" }); +Address.belongsTo(User, { targetKey: 'id' }); +User.hasOne(Address, { sourceKey: 'id' }); async function doStuffWithUser() { const newUser = await User.create({ - name: "Johnny", - preferredName: "John", + name: 'Johnny', + preferredName: 'John', }); console.log(newUser.id, newUser.name, newUser.preferredName); const project = await newUser.createProject({ - name: "first!", + name: 'first!' }); const ourUser = await User.findByPk(1, { include: [User.associations.projects], - rejectOnEmpty: true, // Specifying true here removes `null` from the return type! + rejectOnEmpty: true // Specifying true here removes `null` from the return type! }); // Note the `!` null assertion since TS can't know if we included diff --git a/types/test/typescriptDocs/ModelInitWithAttributesOf.ts b/types/test/typescriptDocs/ModelInitWithAttributesOf.ts deleted file mode 100644 index efe4d0bf7dc3..000000000000 --- a/types/test/typescriptDocs/ModelInitWithAttributesOf.ts +++ /dev/null @@ -1,200 +0,0 @@ -/** - * Keep this file in sync with the code in the "Using with `AttributesOf`" section - * in /docs/manual/other-topics/typescript.md - * - * Don't include this comment in the md file. - */ -import { - Association, DataTypes, HasManyAddAssociationMixin, HasManyCountAssociationsMixin, - HasManyCreateAssociationMixin, HasManyGetAssociationsMixin, HasManyHasAssociationMixin, Model, - ModelDefined, Optional, Sequelize, AttributesOf -} from 'sequelize'; - -const sequelize = new Sequelize('mysql://root:asd123@localhost:3306/mydb'); - -// 'projects' is excluded as it's not an attribute, it's an association. -class User extends Model> { - // id can be undefined during creation when using `autoIncrement` - declare id?: number; - declare name: string; - declare preferredName: string | null; // for nullable fields - - // timestamps! - // createdAt can be undefined during creation - declare readonly createdAt?: Date; - // updatedAt can be undefined during creation - declare readonly updatedAt?: 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; // Note the null assertions! - declare addProject: HasManyAddAssociationMixin; - declare hasProject: HasManyHasAssociationMixin; - declare countProjects: HasManyCountAssociationsMixin; - declare createProject: HasManyCreateAssociationMixin; - - // You can also pre-declare possible inclusions, these will only be populated if you - // actively include a relation. - declare readonly projects?: Project[]; // Note this is optional since it's only populated when explicitly requested in code - - declare static associations: { - projects: Association; - }; -} - -// You can write `extends Model, AttributesOf>` instead, -// but that will do the exact same thing as below -class Project extends Model> { - // id can be undefined during creation when using `autoIncrement` - declare id?: number; - declare ownerId: number; - declare name: string; - - // createdAt can be undefined during creation - declare readonly createdAt?: Date; - // updatedAt can be undefined during creation - declare readonly updatedAt?: Date; -} - -class Address extends Model> { - declare userId: number; - declare address: string; - - // createdAt can be undefined during creation - declare readonly createdAt?: Date; - // updatedAt can be undefined during creation - declare readonly updatedAt?: Date; -} - -// You can also define modules in a functional way -interface NoteAttributes { - id: number; - title: string; - content: string; -} - -// You can also set multiple attributes optional at once -interface NoteCreationAttributes - extends Optional { -} - -Project.init( - { - id: { - type: DataTypes.INTEGER.UNSIGNED, - autoIncrement: true, - primaryKey: true - }, - ownerId: { - type: DataTypes.INTEGER.UNSIGNED, - allowNull: false - }, - name: { - type: new DataTypes.STRING(128), - allowNull: false - }, - }, - { - 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 - } - }, - { - tableName: 'users', - sequelize // passing the `sequelize` instance is required - } -); - -Address.init( - { - userId: { - type: DataTypes.INTEGER.UNSIGNED - }, - address: { - type: new DataTypes.STRING(128), - allowNull: false - } - }, - { - tableName: 'address', - sequelize // passing the `sequelize` instance is required - } -); - -// And with a functional approach defining a module looks like this -const Note: ModelDefined = sequelize.define( - 'Note', - { - id: { - type: DataTypes.INTEGER.UNSIGNED, - autoIncrement: true, - primaryKey: true - }, - title: { - type: new DataTypes.STRING(64), - defaultValue: 'Unnamed Note' - }, - content: { - type: new DataTypes.STRING(4096), - allowNull: false - } - }, - { - tableName: 'notes' - } -); - -// Here we associate which actually populates out pre-declared `association` static and other methods. -User.hasMany(Project, { - sourceKey: 'id', - foreignKey: 'ownerId', - as: 'projects' // this determines the name in `associations`! -}); - -Address.belongsTo(User, { targetKey: 'id' }); -User.hasOne(Address, { sourceKey: 'id' }); - -async function doStuffWithUser() { - const newUser = await User.create({ - name: 'Johnny', - preferredName: 'John', - }); - console.log(newUser.id, newUser.name, newUser.preferredName); - - const project = await newUser.createProject({ - name: 'first!' - }); - - const ourUser = await User.findByPk(1, { - include: [User.associations.projects], - rejectOnEmpty: true // Specifying true here removes `null` from the return type! - }); - - // Note the `!` null assertion since TS can't know if we included - // the model or not - console.log(ourUser.projects![0].name); -} - -(async () => { - await sequelize.sync(); - await doStuffWithUser(); -})(); From 11bcaf6efdf48bf2c35b97a05a58bb7b096f2cd3 Mon Sep 17 00:00:00 2001 From: Guylian Cox Date: Fri, 7 Jan 2022 16:05:35 +0100 Subject: [PATCH 09/21] feat(types): bring back CreationOptional now that I understand branding --- types/lib/model.d.ts | 63 +++++++++++++++++++++----- types/test/attributes-of.ts | 15 ++++-- types/test/models/User.ts | 19 ++++---- types/test/models/UserGroup.ts | 6 +-- types/test/typescriptDocs/ModelInit.ts | 30 ++++++------ 5 files changed, 88 insertions(+), 45 deletions(-) diff --git a/types/lib/model.d.ts b/types/lib/model.d.ts index 37d0c3e6ed66..62c3eff36f85 100644 --- a/types/lib/model.d.ts +++ b/types/lib/model.d.ts @@ -2934,15 +2934,8 @@ export default Model; * Option bag for {@link AttributesOf}. * * - omit: properties to not treat as Attributes. - * - optional: properties that are optional. For use with CreationAttributes. */ -type AttributesOfOptions< - Excluded, - Optional -> = { - omit?: Excluded, - optional?: Optional, -}; +type AttributesOfOptions = { omit?: Excluded }; /** * Utility type to extract Attributes of a given Model. @@ -2984,15 +2977,61 @@ type AttributesOfOptions< */ export type AttributesOf< M extends Model, - Options extends AttributesOfOptions = { omit: never, optional: never } - > = { + Options extends AttributesOfOptions = { omit: never } +> = { + [Key in keyof M as + M[Key] extends AnyFunction ? never + : Key extends keyof Model ? never + // check 'omit' option is provided + : Options['omit'] extends string ? (Key extends Options['omit'] ? never : Key) + : Key + ]: M[Key] +}; + +/** + * Dummy Symbol used as branding by {@link CreationOptional}. + * + * Do not export, Do not use. + */ +declare const CreationAttributeBrand: unique symbol; + +/** + * This is a Branded Type. + * You can use it to tag attributes that can be ommited during Model Creation. + * + * For use with {@link CreationAttributesOf}. + * + * @see CreationAttributesOf + */ +export type CreationOptional = T | { [CreationAttributeBrand]: true }; // this MUST be a union or nothing will be assignable to this type. + +/** + * Utility type to extract Attributes of a given Model. + * + * Works like {@link AttributesOf}, but fields that are tagged using + * {@link CreationOptional} will be optional. + * + * @example + * class User extends Model, CreationAttributesOf> { + * // this attribute is optional in Model#create + * declare id: CreationOptional; + * + * // this attribute is mandatory in Model#create + * declare name: string; + * } + */ +export type CreationAttributesOf< + M extends Model, + Options extends AttributesOfOptions = { omit: never } +> = { [Key in keyof M as M[Key] extends AnyFunction ? never : Key extends keyof Model ? never // check 'omit' option is provided : Options['omit'] extends string ? (Key extends Options['omit'] ? never : Key) : Key - // check 'optional' option is provided - ]: Options['optional'] extends string ? (Key extends Options['optional'] ? (M[Key] | undefined) : M[Key]) + // it is normal that the brand extends the type. + // We're checking if it's in the type. + ]: { [CreationAttributeBrand]: true } extends M[Key] ? (M[Key] | undefined) : M[Key] }; diff --git a/types/test/attributes-of.ts b/types/test/attributes-of.ts index 887536d9701c..1cf537cf2be1 100644 --- a/types/test/attributes-of.ts +++ b/types/test/attributes-of.ts @@ -1,5 +1,5 @@ import { expectTypeOf } from 'expect-type'; -import { AttributesOf, Model } from 'sequelize'; +import { AttributesOf, CreationAttributesOf, CreationOptional, Model } from 'sequelize'; type GroupAttributes = AttributesOf; @@ -18,11 +18,20 @@ class Group extends Model { const fail3: AttributesOf = {}; } + +declare const Brand: unique symbol; + +type Branded = T | { [Brand]: true }; +type BrandedString = Branded; +const works: { [Brand]: true } extends BrandedString ? 1 : 0 = 1; +const brandedString: Branded = 'test'; +const myString: string = brandedString; + type UserAttributes = AttributesOf; -type UserCreationAttributes = AttributesOf; +type UserCreationAttributes = CreationAttributesOf; class User extends Model { - declare id: number; + declare id: CreationOptional; declare name: string | undefined; declare groups: Group[]; diff --git a/types/test/models/User.ts b/types/test/models/User.ts index 10a7d0482885..ba6e630bd6d8 100644 --- a/types/test/models/User.ts +++ b/types/test/models/User.ts @@ -3,34 +3,33 @@ import { BelongsTo, BelongsToCreateAssociationMixin, BelongsToGetAssociationMixin, - BelongsToSetAssociationMixin, + BelongsToSetAssociationMixin, CreationAttributesOf, CreationOptional, DataTypes, FindOptions, Model, ModelStatic, - Op, + Op } from 'sequelize'; import { sequelize } from '../connection'; type NonUserAttributes = 'group'; -type CreationOptionalAttrs = 'id' | 'createdAt' | 'updatedAt' | 'username' | 'lastName' | 'groupId'; export class User extends Model< AttributesOf, - AttributesOf + CreationAttributesOf > { public static associations: { group: BelongsTo; }; - declare id: number; - declare createdAt: Date; - declare updatedAt: Date; + declare id: CreationOptional; + declare createdAt: CreationOptional; + declare updatedAt: CreationOptional; - declare username: string | null; + declare username: CreationOptional; declare firstName: string; - declare lastName: string | null; - declare groupId: number | null; + declare lastName: CreationOptional; + declare groupId: CreationOptional; // mixins for association (optional) declare group?: UserGroup; diff --git a/types/test/models/UserGroup.ts b/types/test/models/UserGroup.ts index 2766709b51de..40451ca29eb0 100644 --- a/types/test/models/UserGroup.ts +++ b/types/test/models/UserGroup.ts @@ -1,5 +1,5 @@ import { - AttributesOf, + AttributesOf, CreationAttributesOf, CreationOptional, DataTypes, HasMany, HasManyAddAssociationMixin, @@ -22,13 +22,13 @@ import { User } from './User'; // function just fine, with a bit less safe type-checking export class UserGroup extends Model< AttributesOf, - AttributesOf + CreationAttributesOf > { public static associations: { users: HasMany }; - declare id: number; + declare id: CreationOptional; declare name: string; // mixins for association (optional) diff --git a/types/test/typescriptDocs/ModelInit.ts b/types/test/typescriptDocs/ModelInit.ts index e9ed9efd67e7..96598e2dcc77 100644 --- a/types/test/typescriptDocs/ModelInit.ts +++ b/types/test/typescriptDocs/ModelInit.ts @@ -7,28 +7,26 @@ import { Association, DataTypes, HasManyAddAssociationMixin, HasManyCountAssociationsMixin, HasManyCreateAssociationMixin, HasManyGetAssociationsMixin, HasManyHasAssociationMixin, Model, - ModelDefined, Optional, Sequelize, AttributesOf + ModelDefined, Optional, Sequelize, AttributesOf, CreationAttributesOf, CreationOptional } from 'sequelize'; const sequelize = new Sequelize('mysql://root:asd123@localhost:3306/mydb'); // 'projects' is excluded as it's not an attribute, it's an association. class User extends Model< - // attributes AttributesOf, - // creation attributes - AttributesOf + CreationAttributesOf > { // id can be undefined during creation when using `autoIncrement` - declare id: number; + declare id: CreationOptional; declare name: string; declare preferredName: string | null; // for nullable fields // timestamps! // createdAt can be undefined during creation - declare readonly createdAt: Date; + declare readonly createdAt: CreationOptional; // updatedAt can be undefined during creation - declare readonly updatedAt: Date; + declare readonly updatedAt: CreationOptional; // Since TS cannot determine model association at compile time // we have to declare them here purely virtually @@ -51,33 +49,31 @@ class User extends Model< // You can write `extends Model, AttributesOf>` instead, // but that will do the exact same thing as below class Project extends Model< - // attributes AttributesOf, - // creation attributes - AttributesOf + CreationAttributesOf > { // id can be undefined during creation when using `autoIncrement` - declare id: number; + declare id: CreationOptional; declare ownerId: number; declare name: string; // createdAt can be undefined during creation - declare readonly createdAt: Date; + declare readonly createdAt: CreationOptional; // updatedAt can be undefined during creation - declare readonly updatedAt: Date; + declare readonly updatedAt: CreationOptional; } class Address extends Model< AttributesOf
, - AttributesOf - > { + CreationAttributesOf
+> { declare userId: number; declare address: string; // createdAt can be undefined during creation - declare readonly createdAt: Date; + declare readonly createdAt: CreationOptional; // updatedAt can be undefined during creation - declare readonly updatedAt: Date; + declare readonly updatedAt: CreationOptional; } Project.init( From 7896f96c3b18b862d3f3a86a97650da336b79fca Mon Sep 17 00:00:00 2001 From: Guylian Cox Date: Fri, 7 Jan 2022 16:17:50 +0100 Subject: [PATCH 10/21] feat(types): add `NonAttribute` branded type --- types/lib/model.d.ts | 41 +++++++++++++++++++++------- types/test/attributes-of.ts | 53 ++++++++++++++++++------------------- 2 files changed, 57 insertions(+), 37 deletions(-) diff --git a/types/lib/model.d.ts b/types/lib/model.d.ts index 62c3eff36f85..dacd6a114ba1 100644 --- a/types/lib/model.d.ts +++ b/types/lib/model.d.ts @@ -2930,6 +2930,20 @@ export type ModelStatic = NonConstructor & { new( export default Model; +/** + * Dummy Symbol used as branding by {@link NonAttribute}. + * + * Do not export, Do not use. + */ +declare const NonAttributeBrand: unique symbol; + +/** + * This is a Branded Type. + * You can use it to tag fields from your class that are NOT attributes. + * They will be ignored by {@link AttributesOf} and {@link CreationAttributesOf} + */ +export type NonAttribute = T | { [NonAttributeBrand]: true }; // this MUST be a union or nothing will be assignable to this type. + /** * Option bag for {@link AttributesOf}. * @@ -2944,6 +2958,7 @@ type AttributesOfOptions = { omit?: Excluded }; * - those inherited from Model (intermediate inheritance works), * - the ones whose type is a function, * - the ones manually excluded using the second parameter. + * - the ones branded using {@link NonAttribute} * * It cannot detect whether something is a getter or not, you should use the `Excluded` * parameter to exclude getter & setters from the attribute list. @@ -2957,22 +2972,28 @@ type AttributesOfOptions = { omit?: Excluded }; * * @example * // listed attributes will be 'id' & 'firstName'. - * // we're excluding the `name` & `test` getters using the `omit` option. - * class User extends Model> { + * // we're excluding the `name` getter & `projects` attribute using the `omit` option. + * class User extends Model> { * id: number; * firstName: string; * - * get name() { return this.firstName; } - * get test() { return ''; } + * // this is a getter, not an attribute. It should not be listed in attributes. + * get name(): string { return this.firstName; } + * // this is an association, it should not be listed in attributes + * projects?: Project[]; * } * * @example - * // You can mark some properties as Optional during creation using the `optional` option. - * - * class User extends Model, AttributesOf> { - * // this attribute will be optional in User.create + * // listed attributes will be 'id' & 'firstName'. + * // we're excluding the `name` getter & `test` attribute using the `NonAttribute` branded type. + * class User extends Model> { * id: number; * firstName: string; + * + * // this is a getter, not an attribute. It should not be listed in attributes. + * get name(): NonAttribute { return this.firstName; } + * // this is an association, it should not be listed in attributes + * projects?: NonAttribute; * } */ export type AttributesOf< @@ -2981,6 +3002,7 @@ export type AttributesOf< > = { [Key in keyof M as M[Key] extends AnyFunction ? never + : { [NonAttributeBrand]: true } extends M[Key] ? never : Key extends keyof Model ? never // check 'omit' option is provided : Options['omit'] extends string ? (Key extends Options['omit'] ? never : Key) @@ -3000,8 +3022,6 @@ declare const CreationAttributeBrand: unique symbol; * You can use it to tag attributes that can be ommited during Model Creation. * * For use with {@link CreationAttributesOf}. - * - * @see CreationAttributesOf */ export type CreationOptional = T | { [CreationAttributeBrand]: true }; // this MUST be a union or nothing will be assignable to this type. @@ -3026,6 +3046,7 @@ export type CreationAttributesOf< > = { [Key in keyof M as M[Key] extends AnyFunction ? never + : { [NonAttributeBrand]: true } extends M[Key] ? never : Key extends keyof Model ? never // check 'omit' option is provided : Options['omit'] extends string ? (Key extends Options['omit'] ? never : Key) diff --git a/types/test/attributes-of.ts b/types/test/attributes-of.ts index 1cf537cf2be1..0c4850eb3de8 100644 --- a/types/test/attributes-of.ts +++ b/types/test/attributes-of.ts @@ -1,31 +1,6 @@ import { expectTypeOf } from 'expect-type'; import { AttributesOf, CreationAttributesOf, CreationOptional, Model } from 'sequelize'; - -type GroupAttributes = AttributesOf; - -class Group extends Model { - declare id: number; -} - -{ - // @ts-expect-error - id should not be missing - const fail1: GroupAttributes = {}; - - // @ts-expect-error - id should not be missing - const fail2: AttributesOf = {}; - - // @ts-expect-error - id should not be missing - const fail3: AttributesOf = {}; -} - - -declare const Brand: unique symbol; - -type Branded = T | { [Brand]: true }; -type BrandedString = Branded; -const works: { [Brand]: true } extends BrandedString ? 1 : 0 = 1; -const brandedString: Branded = 'test'; -const myString: string = brandedString; +import { NonAttribute } from '../lib/model.js'; type UserAttributes = AttributesOf; type UserCreationAttributes = CreationAttributesOf; @@ -34,7 +9,10 @@ class User extends Model { declare id: CreationOptional; declare name: string | undefined; - declare groups: Group[]; + // omitted using `omit` option + declare groups?: Group[]; + // omitted using `NonAttribute` + declare projects?: NonAttribute; instanceMethod() {} static staticMethod() {} @@ -79,3 +57,24 @@ expectTypeOf().not.toBeAny(); id: 1, }; } + +type GroupAttributes = AttributesOf; + +class Group extends Model { + declare id: number; +} + +{ + // @ts-expect-error - id should not be missing + const fail1: GroupAttributes = {}; + + // @ts-expect-error - id should not be missing + const fail2: AttributesOf = {}; + + // @ts-expect-error - id should not be missing + const fail3: AttributesOf = {}; +} + +class Project extends Model> { + declare id: number; +} From 60589bc9f0ed2f5adfc9d2c8c330f119335d698d Mon Sep 17 00:00:00 2001 From: Guylian Cox Date: Fri, 7 Jan 2022 16:46:50 +0100 Subject: [PATCH 11/21] docs(types): document new attribute declaration system --- docs/manual/other-topics/typescript.md | 454 ++++++++----------------- types/lib/model.d.ts | 2 + types/test/typescriptDocs/ModelInit.ts | 17 +- 3 files changed, 162 insertions(+), 311 deletions(-) diff --git a/docs/manual/other-topics/typescript.md b/docs/manual/other-topics/typescript.md index 186b5bd9e1a7..6056cf08d5e5 100644 --- a/docs/manual/other-topics/typescript.md +++ b/docs/manual/other-topics/typescript.md @@ -1,8 +1,9 @@ # TypeScript -Since v5, Sequelize provides its own TypeScript definitions. Please note that only TS >= 3.1 is supported. +Since v5, Sequelize provides its own TypeScript definitions. Please note that only TS >= 4.1 is supported. -As Sequelize heavily relies on runtime property assignments, TypeScript won't be very useful out of the box. A decent amount of manual type declarations are needed to make models workable. +As Sequelize heavily relies on runtime property assignments, TypeScript won't be very useful out of the box. +A decent amount of manual type declarations are needed to make models workable. ## Installation @@ -13,326 +14,92 @@ In order to avoid installation bloat for non TS users, you must install the foll ## Usage -Example of a minimal TypeScript project with strict type-checking for attributes. - **Important**: You must use `declare` on your class properties typings to ensure TypeScript does not emit those class properties. See [Caveat with Public Class Fields](./model-basics.html#caveat-with-public-class-fields) - +Sequelize Models accept two generic types to define what the model's Attributes & Creation Attributes are like: ```typescript -import { - Association, DataTypes, HasManyAddAssociationMixin, HasManyCountAssociationsMixin, - HasManyCreateAssociationMixin, HasManyGetAssociationsMixin, HasManyHasAssociationMixin, Model, - ModelDefined, Optional, Sequelize -} from "sequelize"; - -const sequelize = new Sequelize("mysql://root:asd123@localhost:3306/mydb"); - -// These are all the attributes in the User model -interface UserAttributes { - id: number; - name: string; - preferredName: string | null; -} +import { Model, Optional } from 'sequelize'; -// Some attributes are optional in `User.build` and `User.create` calls -interface UserCreationAttributes extends Optional {} +// We don't recommend doing this. Read on for the new way of declaring Model typings. -class User extends Model - implements UserAttributes { - declare id: number; // Note that the `null assertion` `!` is required in strict mode. - declare name: string; - declare preferredName: string | null; // for nullable fields +type UserAttributes = { + id: number, + name: string, + // other attributes... +}; - // timestamps! - declare readonly createdAt: Date; - declare readonly updatedAt: Date; +// we're telling the Model that 'id' is optional +// when creating an instance of the model (such as using Model.create()). +type UserCreationAttributes = Optional; - // 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; // Note the null assertions! - declare addProject: HasManyAddAssociationMixin; - declare hasProject: HasManyHasAssociationMixin; - declare countProjects: HasManyCountAssociationsMixin; - declare createProject: HasManyCreateAssociationMixin; - - // You can also pre-declare possible inclusions, these will only be populated if you - // actively include a relation. - declare readonly projects?: Project[]; // Note this is optional since it's only populated when explicitly requested in code - - declare static associations: { - projects: Association; - }; -} - -interface ProjectAttributes { - id: number; - ownerId: number; - name: string; - description?: string; -} - -interface ProjectCreationAttributes extends Optional {} - -class Project extends Model - implements ProjectAttributes { +class User extends Model { declare id: number; - declare ownerId: number; - declare name: string; - - declare readonly createdAt: Date; - declare readonly updatedAt: Date; -} - -interface AddressAttributes { - userId: number; - address: string; -} - -// You can write `extends Model` instead, -// but that will do the exact same thing as below -class Address extends Model implements AddressAttributes { - declare userId: number; - declare address: string; - - declare readonly createdAt: Date; - declare readonly updatedAt: Date; + declare string: number; + // other attributes... } - -// You can also define modules in a functional way -interface NoteAttributes { - id: number; - title: string; - content: string; -} - -// You can also set multiple attributes optional at once -interface NoteCreationAttributes - extends Optional {} - -Project.init( - { - id: { - type: DataTypes.INTEGER.UNSIGNED, - autoIncrement: true, - primaryKey: true, - }, - ownerId: { - type: DataTypes.INTEGER.UNSIGNED, - allowNull: false, - }, - name: { - type: new DataTypes.STRING(128), - allowNull: false, - }, - description: { - type: new DataTypes.STRING(128), - allowNull: true, - }, - }, - { - 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, - }, - }, - { - tableName: "users", - sequelize, // passing the `sequelize` instance is required - } -); - -Address.init( - { - userId: { - type: DataTypes.INTEGER.UNSIGNED, - }, - address: { - type: new DataTypes.STRING(128), - allowNull: false, - }, - }, - { - tableName: "address", - sequelize, // passing the `sequelize` instance is required - } -); - -// And with a functional approach defining a module looks like this -const Note: ModelDefined< - NoteAttributes, - NoteCreationAttributes -> = sequelize.define( - "Note", - { - id: { - type: DataTypes.INTEGER.UNSIGNED, - autoIncrement: true, - primaryKey: true, - }, - title: { - type: new DataTypes.STRING(64), - defaultValue: "Unnamed Note", - }, - content: { - type: new DataTypes.STRING(4096), - allowNull: false, - }, - }, - { - tableName: "notes", - } -); - -// Here we associate which actually populates out pre-declared `association` static and other methods. -User.hasMany(Project, { - sourceKey: "id", - foreignKey: "ownerId", - as: "projects", // this determines the name in `associations`! -}); - -Address.belongsTo(User, { targetKey: "id" }); -User.hasOne(Address, { sourceKey: "id" }); - -async function doStuffWithUser() { - const newUser = await User.create({ - name: "Johnny", - preferredName: "John", - }); - console.log(newUser.id, newUser.name, newUser.preferredName); - - const project = await newUser.createProject({ - name: "first!", - ownerId: 123, - }); - - const ourUser = await User.findByPk(1, { - include: [User.associations.projects], - rejectOnEmpty: true, // Specifying true here removes `null` from the return type! - }); - - // Note the `!` null assertion since TS can't know if we included - // the model or not - console.log(ourUser.projects![0].name); -} - -(async () => { - await sequelize.sync(); - await doStuffWithUser(); -})(); ``` -### Usage without strict types for attributes - -The typings for Sequelize v5 allowed you to define models without specifying types for the attributes. This is still possible for backwards compatibility and for cases where you feel strict typing for attributes isn't worth it. - - - -```ts -import { Sequelize, Model, DataTypes } from "sequelize"; - -const sequelize = new Sequelize("mysql://root:asd123@localhost:3306/mydb"); - -class User extends Model { - public id!: number; // Note that the `null assertion` `!` is required in strict mode. - public name!: string; - public preferredName!: string | null; // for nullable fields -} - -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, - }, - }, - { - tableName: "users", - sequelize, // passing the `sequelize` instance is required - } -); - -async function doStuffWithUserModel() { - const newUser = await User.create({ - name: "Johnny", - preferredName: "John", - }); - console.log(newUser.id, newUser.name, newUser.preferredName); +This solution is verbose. Sequelize v6 provides new utility types that will drastically reduce the amount +of boilerplate necessary: `AttributeOf`, and `CreationAttributeOf`. They will extract Attribute typings +directly from the Model: - const foundUser = await User.findOne({ where: { name: "Johnny" } }); - if (foundUser === null) return; - console.log(foundUser.name); +```typescript +import { Model, AttributesOf, CreationAttributesOf, CreationOptional } from 'sequelize'; + +// order of AttributesOf & CreationAttributesOf is important. +class User extends Model, CreationAttributesOf> { + // 'CreationOptional' is a special type that marks the field as optional + // when creating an instance of the model (such as using Model.create()). + declare id: CreationOptional; + declare string: number; + // other attributes... } ``` -### Using with `AttributesOf` - -`AttributeOf` is a utility type you can use to automatically extract attributes from your class definition. -It's an alternative solution designed to reduce the boilerplate of the above solution. - -Some caveats of this solution: +Important things to know about `AttributesOf` & `CreationAttributes` work: They will select all declared properties of the class except: -- it's not possible to use `AttributesOf` for `CreationAttributes`. - This example opted for making properties that are not set until saved always optional instead. -- `AttributesOf` cannot whether a field is a getter, so they are not excluded by default. Use the second parameter to exclude getters. - eg. `AttributesOf` will exclude the field "projects" from the attribute list. -- `AttributesOf` excludes fields that are inherited from `Model`. +- Static fields and methods. +- Methods (anything whose type is a function). +- Those whose type uses the branded type `NonAttribute`. +- Those excluded by using AttributesOf like this: `AttributesOf`. +- Those declared by the Model superclass (but not intermediary classes!). If one of your attributes shares the same name as one of the properties of `Model`, change its name. Doing this is likely to cause issues anyway. +- Getter & setters are not automatically excluded. Set their return / parameter type to `NonAttribute`, + or add them to `omit` to exclude them. -Here is the same example as above, but using `AttributeOf`. +`CreationAttributes` works the same way as `AttributesOf` with one exception: Properties typed using the `CreationOptional` type +will be marked as optional. - +You only need to use `CreationOptional` & `NonAttribute` on class instance fields or getters. + +Example of a minimal TypeScript project with strict type-checking for attributes: + + ```typescript import { Association, DataTypes, HasManyAddAssociationMixin, HasManyCountAssociationsMixin, HasManyCreateAssociationMixin, HasManyGetAssociationsMixin, HasManyHasAssociationMixin, Model, - ModelDefined, Optional, Sequelize, AttributesOf + ModelDefined, Optional, Sequelize, AttributesOf, CreationAttributesOf, CreationOptional, NonAttribute } from 'sequelize'; const sequelize = new Sequelize('mysql://root:asd123@localhost:3306/mydb'); // 'projects' is excluded as it's not an attribute, it's an association. -class User extends Model> { +class User extends Model, CreationAttributesOf> { // id can be undefined during creation when using `autoIncrement` - declare id?: number; + declare id: CreationOptional; declare name: string; declare preferredName: string | null; // for nullable fields // timestamps! // createdAt can be undefined during creation - declare readonly createdAt?: Date; + declare readonly createdAt: CreationOptional; // updatedAt can be undefined during creation - declare readonly updatedAt?: Date; + declare readonly updatedAt: CreationOptional; // Since TS cannot determine model association at compile time // we have to declare them here purely virtually @@ -347,6 +114,12 @@ class User extends Model> { // actively include a relation. declare readonly projects?: 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 { + return this.name; + } + declare static associations: { projects: Association; }; @@ -354,38 +127,36 @@ class User extends Model> { // You can write `extends Model, AttributesOf>` instead, // but that will do the exact same thing as below -class Project extends Model> { +class Project extends Model< + AttributesOf, + CreationAttributesOf +> { // id can be undefined during creation when using `autoIncrement` - declare id?: number; + declare id: CreationOptional; declare ownerId: number; declare name: string; + // `owner` is an eagerly-loaded association. + // We tag it as `NonAttribute` + declare owner?: NonAttribute; + // createdAt can be undefined during creation - declare readonly createdAt?: Date; + declare readonly createdAt: CreationOptional; // updatedAt can be undefined during creation - declare readonly updatedAt?: Date; + declare readonly updatedAt: CreationOptional; } -class Address extends Model> { +class Address extends Model< + AttributesOf
, + CreationAttributesOf
+> { declare userId: number; declare address: string; // createdAt can be undefined during creation - declare readonly createdAt?: Date; + declare readonly createdAt: CreationOptional; // updatedAt can be undefined during creation - declare readonly updatedAt?: Date; -} - -// You can also define modules in a functional way -interface NoteAttributes { - id: number; - title: string; - content: string; -} - -// You can also set multiple attributes optional at once -interface NoteCreationAttributes - extends Optional { + declare readonly updatedAt: CreationOptional; } Project.init( @@ -403,6 +174,8 @@ Project.init( type: new DataTypes.STRING(128), allowNull: false }, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE, }, { sequelize, @@ -424,7 +197,9 @@ User.init( preferredName: { type: new DataTypes.STRING(128), allowNull: true - } + }, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE, }, { tableName: 'users', @@ -440,7 +215,9 @@ Address.init( address: { type: new DataTypes.STRING(128), allowNull: false - } + }, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE, }, { tableName: 'address', @@ -448,9 +225,21 @@ Address.init( } ); +// You can also define modules in a functional way +interface NoteAttributes { + id: number; + title: string; + content: string; +} + +// You can also set multiple attributes optional at once +type NoteCreationAttributes = Optional; + // And with a functional approach defining a module looks like this -const Note: ModelDefined = sequelize.define( +const Note: ModelDefined< + NoteAttributes, + NoteCreationAttributes +> = sequelize.define( 'Note', { id: { @@ -509,6 +298,59 @@ async function doStuffWithUser() { })(); ``` +### Usage without strict types for attributes + +The typings for Sequelize v5 allowed you to define models without specifying types for the attributes. This is still possible for backwards compatibility and for cases where you feel strict typing for attributes isn't worth it. + + + +```ts +import { Sequelize, Model, DataTypes } from "sequelize"; + +const sequelize = new Sequelize("mysql://root:asd123@localhost:3306/mydb"); + +class User extends Model { + declare id: number; + declare name: string; + declare preferredName: string | null; +} + +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, + }, + }, + { + tableName: "users", + sequelize, // passing the `sequelize` instance is required + } +); + +async function doStuffWithUserModel() { + const newUser = await User.create({ + name: "Johnny", + preferredName: "John", + }); + console.log(newUser.id, newUser.name, newUser.preferredName); + + const foundUser = await User.findOne({ where: { name: "Johnny" } }); + if (foundUser === null) return; + console.log(foundUser.name); +} +``` + ## Usage of `sequelize.define` In Sequelize versions before v5, the default way of defining a model involved using `sequelize.define`. It's still possible to define models with that, and you can also add typings to these models using interfaces. diff --git a/types/lib/model.d.ts b/types/lib/model.d.ts index dacd6a114ba1..9096b2803225 100644 --- a/types/lib/model.d.ts +++ b/types/lib/model.d.ts @@ -2942,6 +2942,7 @@ declare const NonAttributeBrand: unique symbol; * You can use it to tag fields from your class that are NOT attributes. * They will be ignored by {@link AttributesOf} and {@link CreationAttributesOf} */ +// TODO: Not compatible with Arrays. Need to find a solution. export type NonAttribute = T | { [NonAttributeBrand]: true }; // this MUST be a union or nothing will be assignable to this type. /** @@ -3023,6 +3024,7 @@ declare const CreationAttributeBrand: unique symbol; * * For use with {@link CreationAttributesOf}. */ +// TODO: Not compatible with Arrays. Need to find a solution. export type CreationOptional = T | { [CreationAttributeBrand]: true }; // this MUST be a union or nothing will be assignable to this type. /** diff --git a/types/test/typescriptDocs/ModelInit.ts b/types/test/typescriptDocs/ModelInit.ts index 96598e2dcc77..71ecdae111b9 100644 --- a/types/test/typescriptDocs/ModelInit.ts +++ b/types/test/typescriptDocs/ModelInit.ts @@ -7,16 +7,13 @@ import { Association, DataTypes, HasManyAddAssociationMixin, HasManyCountAssociationsMixin, HasManyCreateAssociationMixin, HasManyGetAssociationsMixin, HasManyHasAssociationMixin, Model, - ModelDefined, Optional, Sequelize, AttributesOf, CreationAttributesOf, CreationOptional + ModelDefined, Optional, Sequelize, AttributesOf, CreationAttributesOf, CreationOptional, NonAttribute } from 'sequelize'; const sequelize = new Sequelize('mysql://root:asd123@localhost:3306/mydb'); // 'projects' is excluded as it's not an attribute, it's an association. -class User extends Model< - AttributesOf, - CreationAttributesOf -> { +class User extends Model, CreationAttributesOf> { // id can be undefined during creation when using `autoIncrement` declare id: CreationOptional; declare name: string; @@ -41,6 +38,12 @@ class User extends Model< // actively include a relation. declare readonly projects?: 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 { + return this.name; + } + declare static associations: { projects: Association; }; @@ -57,6 +60,10 @@ class Project extends Model< declare ownerId: number; declare name: string; + // `owner` is an eagerly-loaded association. + // We tag it as `NonAttribute` + declare owner?: NonAttribute; + // createdAt can be undefined during creation declare readonly createdAt: CreationOptional; // updatedAt can be undefined during creation From b4adc2a20268347c5e88d5f9b67300078923c5a3 Mon Sep 17 00:00:00 2001 From: Guylian Cox Date: Fri, 7 Jan 2022 17:32:54 +0100 Subject: [PATCH 12/21] fix(types): fix branded array support --- types/lib/model.d.ts | 26 +++++++++++-- types/test/attributes-of.ts | 54 ++++++++++++++++++++++---- types/test/typescriptDocs/ModelInit.ts | 2 +- 3 files changed, 70 insertions(+), 12 deletions(-) diff --git a/types/lib/model.d.ts b/types/lib/model.d.ts index 9096b2803225..a4168e963bae 100644 --- a/types/lib/model.d.ts +++ b/types/lib/model.d.ts @@ -2936,14 +2936,20 @@ export default Model; * Do not export, Do not use. */ declare const NonAttributeBrand: unique symbol; +interface NonAttributeBrandedArray extends Array { + [NonAttributeBrand]: true +} /** * This is a Branded Type. * You can use it to tag fields from your class that are NOT attributes. * They will be ignored by {@link AttributesOf} and {@link CreationAttributesOf} */ -// TODO: Not compatible with Arrays. Need to find a solution. -export type NonAttribute = T | { [NonAttributeBrand]: true }; // this MUST be a union or nothing will be assignable to this type. +export type NonAttribute = T extends Array + // Arrays are a special case when branding. Both sides need to be an array, + // otherwise property access breaks. + ? T | NonAttributeBrandedArray + : T | { [NonAttributeBrand]: true }; // this MUST be a union or nothing will be assignable to this type. /** * Option bag for {@link AttributesOf}. @@ -3004,6 +3010,8 @@ export type AttributesOf< [Key in keyof M as M[Key] extends AnyFunction ? never : { [NonAttributeBrand]: true } extends M[Key] ? never + // array special case + : M[Key] extends NonAttributeBrandedArray ? never : Key extends keyof Model ? never // check 'omit' option is provided : Options['omit'] extends string ? (Key extends Options['omit'] ? never : Key) @@ -3017,6 +3025,9 @@ export type AttributesOf< * Do not export, Do not use. */ declare const CreationAttributeBrand: unique symbol; +interface CreationAttributeBrandedArray extends Array { + [CreationAttributeBrand]: true +} /** * This is a Branded Type. @@ -3024,8 +3035,11 @@ declare const CreationAttributeBrand: unique symbol; * * For use with {@link CreationAttributesOf}. */ -// TODO: Not compatible with Arrays. Need to find a solution. -export type CreationOptional = T | { [CreationAttributeBrand]: true }; // this MUST be a union or nothing will be assignable to this type. +export type CreationOptional = T extends Array + // Arrays are a special case when branding. Both sides need to be an array, + // otherwise property access breaks. + ? T | CreationAttributeBrandedArray + : T | { [CreationAttributeBrand]: true }; // this MUST be a union or nothing will be assignable to this type. /** * Utility type to extract Attributes of a given Model. @@ -3049,6 +3063,8 @@ export type CreationAttributesOf< [Key in keyof M as M[Key] extends AnyFunction ? never : { [NonAttributeBrand]: true } extends M[Key] ? never + // array special case + : M[Key] extends NonAttributeBrandedArray ? never : Key extends keyof Model ? never // check 'omit' option is provided : Options['omit'] extends string ? (Key extends Options['omit'] ? never : Key) @@ -3056,5 +3072,7 @@ export type CreationAttributesOf< // it is normal that the brand extends the type. // We're checking if it's in the type. ]: { [CreationAttributeBrand]: true } extends M[Key] ? (M[Key] | undefined) + // array special case + : M[Key] extends CreationAttributeBrandedArray ? (M[Key] | undefined) : M[Key] }; diff --git a/types/test/attributes-of.ts b/types/test/attributes-of.ts index 0c4850eb3de8..fdd570fb5d03 100644 --- a/types/test/attributes-of.ts +++ b/types/test/attributes-of.ts @@ -1,18 +1,18 @@ import { expectTypeOf } from 'expect-type'; -import { AttributesOf, CreationAttributesOf, CreationOptional, Model } from 'sequelize'; -import { NonAttribute } from '../lib/model.js'; +import { AttributesOf, CreationAttributesOf, CreationOptional, Model, NonAttribute } from 'sequelize'; type UserAttributes = AttributesOf; type UserCreationAttributes = CreationAttributesOf; class User extends Model { declare id: CreationOptional; - declare name: string | undefined; + declare name: string; + declare anArray: CreationOptional; // omitted using `omit` option - declare groups?: Group[]; + declare groups: Group[]; // omitted using `NonAttribute` - declare projects?: NonAttribute; + declare projects: NonAttribute; instanceMethod() {} static staticMethod() {} @@ -20,10 +20,19 @@ class User extends Model { expectTypeOf().not.toBeAny(); +{ + class Test extends Model> { + declare id: NonAttribute; + } + + const win: Test['_attributes'] = {}; +} + { const win: UserAttributes = { id: 1, name: '', + anArray: [''], }; const fail1: UserAttributes = { @@ -33,7 +42,7 @@ expectTypeOf().not.toBeAny(); extra: '' }; -// @ts-expect-error - 'name' should be present + // @ts-expect-error - 'name' should be present const fail2: UserAttributes = { id: 1, }; @@ -43,6 +52,7 @@ expectTypeOf().not.toBeAny(); const win: UserCreationAttributes = { id: undefined, name: '', + anArray: undefined, }; const fail1: UserCreationAttributes = { @@ -52,9 +62,11 @@ expectTypeOf().not.toBeAny(); extra: '' }; -// @ts-expect-error missing 'name' const fail2: UserCreationAttributes = { id: 1, + // @ts-expect-error name cannot be undefined + name: undefined, + anArray: undefined, }; } @@ -78,3 +90,31 @@ class Group extends Model { class Project extends Model> { declare id: number; } + +// brands: + +{ + // ensure branding does not break arrays. + const brandedArray: NonAttribute = ['']; + const anArray: string[] = brandedArray; + const item: string = brandedArray[0]; +} + +{ + // ensure branding does not break objects + const brandedObject: NonAttribute> = {}; + const anObject: Record = brandedObject; + const item: string = brandedObject.key; +} + +{ + // ensure branding does not break primitives + const brandedString: NonAttribute = ''; + const aString: string = brandedString; +} + +{ + // ensure branding does not break instances + const brandedUser: NonAttribute = new User(); + const aUser: User = brandedUser; +} diff --git a/types/test/typescriptDocs/ModelInit.ts b/types/test/typescriptDocs/ModelInit.ts index 71ecdae111b9..56e104fdf69b 100644 --- a/types/test/typescriptDocs/ModelInit.ts +++ b/types/test/typescriptDocs/ModelInit.ts @@ -36,7 +36,7 @@ class User extends Model, CreationAttri // You can also pre-declare possible inclusions, these will only be populated if you // actively include a relation. - declare readonly projects?: Project[]; // Note this is optional since it's only populated when explicitly requested in code + declare readonly projects?: NonAttribute; // 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. From a3bbc840a13b8ac380c90fa8ca9cabe5ca5101c1 Mon Sep 17 00:00:00 2001 From: Guylian Cox Date: Fri, 7 Jan 2022 19:20:10 +0100 Subject: [PATCH 13/21] fix(types): fix array brand typing again --- types/lib/model.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/lib/model.d.ts b/types/lib/model.d.ts index a4168e963bae..7a463d55b508 100644 --- a/types/lib/model.d.ts +++ b/types/lib/model.d.ts @@ -3073,6 +3073,6 @@ export type CreationAttributesOf< // We're checking if it's in the type. ]: { [CreationAttributeBrand]: true } extends M[Key] ? (M[Key] | undefined) // array special case - : M[Key] extends CreationAttributeBrandedArray ? (M[Key] | undefined) + : CreationAttributeBrandedArray extends M[Key] ? (M[Key] | undefined) : M[Key] }; From 17b769914f22dfb4d376200d98fc25b176e28674 Mon Sep 17 00:00:00 2001 From: ephys Date: Sat, 8 Jan 2022 14:48:03 +0100 Subject: [PATCH 14/21] feat(types): add Attributes/CreationAttributes, rename AttributesOf deprecated Model#_attributes and Model#_creationAttributes --- docs/manual/other-topics/typescript.md | 16 +- types/lib/associations/belongs-to-many.d.ts | 6 +- types/lib/associations/belongs-to.d.ts | 4 +- types/lib/associations/has-many.d.ts | 10 +- types/lib/associations/has-one.d.ts | 4 +- types/lib/hooks.d.ts | 22 +- types/lib/model.d.ts | 233 ++++++++++-------- types/lib/query-interface.d.ts | 12 +- types/lib/sequelize.d.ts | 8 +- types/lib/utils.d.ts | 6 +- .../{attributes-of.ts => infer-attributes.ts} | 25 +- types/test/models/User.ts | 12 +- types/test/models/UserGroup.ts | 8 +- types/test/typescriptDocs/ModelInit.ts | 14 +- 14 files changed, 212 insertions(+), 168 deletions(-) rename types/test/{attributes-of.ts => infer-attributes.ts} (76%) diff --git a/docs/manual/other-topics/typescript.md b/docs/manual/other-topics/typescript.md index 6056cf08d5e5..09dbace4254d 100644 --- a/docs/manual/other-topics/typescript.md +++ b/docs/manual/other-topics/typescript.md @@ -83,13 +83,13 @@ Example of a minimal TypeScript project with strict type-checking for attributes import { Association, DataTypes, HasManyAddAssociationMixin, HasManyCountAssociationsMixin, HasManyCreateAssociationMixin, HasManyGetAssociationsMixin, HasManyHasAssociationMixin, Model, - ModelDefined, Optional, Sequelize, AttributesOf, CreationAttributesOf, CreationOptional, NonAttribute + ModelDefined, Optional, Sequelize, InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute } from 'sequelize'; const sequelize = new Sequelize('mysql://root:asd123@localhost:3306/mydb'); // 'projects' is excluded as it's not an attribute, it's an association. -class User extends Model, CreationAttributesOf> { +class User extends Model, InferCreationAttributes> { // id can be undefined during creation when using `autoIncrement` declare id: CreationOptional; declare name: string; @@ -112,7 +112,7 @@ class User extends Model, CreationAttri // You can also pre-declare possible inclusions, these will only be populated if you // actively include a relation. - declare readonly projects?: Project[]; // Note this is optional since it's only populated when explicitly requested in code + declare readonly projects?: NonAttribute; // 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. @@ -125,11 +125,9 @@ class User extends Model, CreationAttri }; } -// You can write `extends Model, AttributesOf>` instead, -// but that will do the exact same thing as below class Project extends Model< - AttributesOf, - CreationAttributesOf + InferAttributes, + InferCreationAttributes > { // id can be undefined during creation when using `autoIncrement` declare id: CreationOptional; @@ -147,8 +145,8 @@ class Project extends Model< } class Address extends Model< - AttributesOf
, - CreationAttributesOf
+ InferAttributes
, + InferCreationAttributes
> { declare userId: number; declare address: string; diff --git a/types/lib/associations/belongs-to-many.d.ts b/types/lib/associations/belongs-to-many.d.ts index b7106ffb197b..7852bc081a2b 100644 --- a/types/lib/associations/belongs-to-many.d.ts +++ b/types/lib/associations/belongs-to-many.d.ts @@ -1,6 +1,6 @@ import { BulkCreateOptions, - CreateOptions, + CreateOptions, CreationAttributes, Filterable, FindAttributeOptions, FindOptions, @@ -9,7 +9,7 @@ import { Model, ModelCtor, ModelType, - Transactionable + Transactionable, } from '../model'; import { Association, AssociationScope, ForeignKeyOptions, ManyToManyOptions, MultiAssociationAccessors } from './base'; @@ -303,7 +303,7 @@ export interface BelongsToManyCreateAssociationMixinOptions extends CreateOption * @see Instance */ export type BelongsToManyCreateAssociationMixin = ( - values?: TModel['_creationAttributes'], + values?: CreationAttributes, options?: BelongsToManyCreateAssociationMixinOptions ) => Promise; diff --git a/types/lib/associations/belongs-to.d.ts b/types/lib/associations/belongs-to.d.ts index fd2a5e356b2a..19d50859290e 100644 --- a/types/lib/associations/belongs-to.d.ts +++ b/types/lib/associations/belongs-to.d.ts @@ -1,5 +1,5 @@ import { DataType } from '../data-types'; -import { CreateOptions, FindOptions, Model, ModelCtor, SaveOptions } from '../model'; +import { CreateOptions, CreationAttributes, FindOptions, Model, ModelCtor, SaveOptions } from '../model'; import { Association, AssociationOptions, SingleAssociationAccessors } from './base'; // type ModelCtor = InstanceType; @@ -117,7 +117,7 @@ export interface BelongsToCreateAssociationMixinOptions * @see Instance */ export type BelongsToCreateAssociationMixin = ( - values?: TModel['_creationAttributes'], + values?: CreationAttributes, options?: BelongsToCreateAssociationMixinOptions ) => Promise; diff --git a/types/lib/associations/has-many.d.ts b/types/lib/associations/has-many.d.ts index 248caa7c988d..7b8903131933 100644 --- a/types/lib/associations/has-many.d.ts +++ b/types/lib/associations/has-many.d.ts @@ -1,12 +1,12 @@ import { DataType } from '../data-types'; import { - CreateOptions, + CreateOptions, CreationAttributes, Filterable, FindOptions, InstanceUpdateOptions, Model, ModelCtor, - Transactionable + Transactionable, } from '../model'; import { Association, ManyToManyOptions, MultiAssociationAccessors } from './base'; import { MakeUndefinedOptional } from '../utils'; @@ -212,10 +212,10 @@ export interface HasManyCreateAssociationMixinOptions extends CreateOptions */ export type HasManyCreateAssociationMixin< TModel extends Model, - TForeignKey extends keyof TModel['_creationAttributes'] = never, - TScope extends keyof TModel['_creationAttributes'] = never + TForeignKey extends keyof CreationAttributes = never, + TScope extends keyof CreationAttributes = never > = ( - values?: Omit, TForeignKey | TScope>, + values?: Omit, TForeignKey | TScope>, options?: HasManyCreateAssociationMixinOptions ) => Promise; diff --git a/types/lib/associations/has-one.d.ts b/types/lib/associations/has-one.d.ts index e81784b3d88a..692b9e1efcbf 100644 --- a/types/lib/associations/has-one.d.ts +++ b/types/lib/associations/has-one.d.ts @@ -1,5 +1,5 @@ import { DataType } from '../data-types'; -import { CreateOptions, FindOptions, Model, ModelCtor, SaveOptions } from '../model'; +import { CreateOptions, CreationAttributes, FindOptions, Model, ModelCtor, SaveOptions } from '../model'; import { Association, AssociationOptions, SingleAssociationAccessors } from './base'; /** @@ -114,6 +114,6 @@ export interface HasOneCreateAssociationMixinOptions extends HasOneSetAssociatio * @see Instance */ export type HasOneCreateAssociationMixin = ( - values?: TModel['_creationAttributes'], + values?: CreationAttributes, options?: HasOneCreateAssociationMixinOptions ) => Promise; diff --git a/types/lib/hooks.d.ts b/types/lib/hooks.d.ts index f62f7f61573e..53a5761d703f 100644 --- a/types/lib/hooks.d.ts +++ b/types/lib/hooks.d.ts @@ -1,4 +1,4 @@ -import { ModelType } from '../index'; +import { Attributes, CreationAttributes, ModelType } from '../index'; import { ValidationOptions } from './instance-validator'; import Model, { BulkCreateOptions, @@ -97,11 +97,17 @@ export class Hooks< /** * A similar dummy variable that doesn't exist on the real object. Do not * try to access this in real code. + * + * @deprecated This property will become a Symbol in v7 to prevent collisions. + * Use Attributes instead of this property to be forward-compatible. */ _attributes: TModelAttributes; /** * A similar dummy variable that doesn't exist on the real object. Do not * try to access this in real code. + * + * @deprecated This property will become a Symbol in v7 to prevent collisions. + * Use CreationAttributes instead of this property to be forward-compatible. */ _creationAttributes: TCreationAttributes; @@ -113,20 +119,20 @@ export class Hooks< */ public static addHook< H extends Hooks, - K extends keyof SequelizeHooks + K extends keyof SequelizeHooks, CreationAttributes> >( this: HooksStatic, hookType: K, name: string, - fn: SequelizeHooks[K] + fn: SequelizeHooks, CreationAttributes>[K] ): HooksCtor; public static addHook< H extends Hooks, - K extends keyof SequelizeHooks + K extends keyof SequelizeHooks, CreationAttributes> >( this: HooksStatic, hookType: K, - fn: SequelizeHooks[K] + fn: SequelizeHooks, CreationAttributes>[K] ): HooksCtor; /** @@ -134,7 +140,7 @@ export class Hooks< */ public static removeHook( this: HooksStatic, - hookType: keyof SequelizeHooks, + hookType: keyof SequelizeHooks, CreationAttributes>, name: string, ): HooksCtor; @@ -143,11 +149,11 @@ export class Hooks< */ public static hasHook( this: HooksStatic, - hookType: keyof SequelizeHooks, + hookType: keyof SequelizeHooks, CreationAttributes>, ): boolean; public static hasHooks( this: HooksStatic, - hookType: keyof SequelizeHooks, + hookType: keyof SequelizeHooks, CreationAttributes>, ): boolean; /** diff --git a/types/lib/model.d.ts b/types/lib/model.d.ts index 7a463d55b508..4fb221aa97f1 100644 --- a/types/lib/model.d.ts +++ b/types/lib/model.d.ts @@ -1398,13 +1398,13 @@ export interface ModelOptions { * Define the default search scope to use for this model. Scopes have the same form as the options passed to * find / findAll. */ - defaultScope?: FindOptions; + defaultScope?: FindOptions>; /** * More scopes, defined in the same way as defaultScope above. See `Model.scope` for more information about * how scopes are defined, and what you can do with them */ - scopes?: ModelScopeOptions; + scopes?: ModelScopeOptions>; /** * Don't persits null values. This means that all columns with null values will not be saved. @@ -1504,7 +1504,7 @@ export interface ModelOptions { * See Hooks for more information about hook * functions and their signatures. Each property can either be a function, or an array of functions. */ - hooks?: Partial>; + hooks?: Partial>>; /** * An object of model wide validations. Validations have access to all model values via `this`. If the @@ -1571,11 +1571,18 @@ export abstract class Model * ): Promise; * ``` + * + * @deprecated This property will become a Symbol in v7 to prevent collisions. + * Use Attributes instead of this property to be forward-compatible. */ _attributes: TModelAttributes; + /** * A similar dummy variable that doesn't exist on the real object. Do not * try to access this in real code. + * + * @deprecated This property will become a Symbol in v7 to prevent collisions. + * Use CreationAttributes instead of this property to be forward-compatible. */ _creationAttributes: TCreationAttributes; @@ -1664,7 +1671,7 @@ export abstract class Model, M extends InstanceType>( this: MS, - attributes: ModelAttributes, options: InitOptions + attributes: ModelAttributes>, options: InitOptions ): MS; /** @@ -1782,13 +1789,13 @@ export abstract class Model( this: ModelStatic, name: string, - scope: FindOptions, + scope: FindOptions>, options?: AddScopeOptions ): void; public static addScope( this: ModelStatic, name: string, - scope: (...args: readonly any[]) => FindOptions, + scope: (...args: readonly any[]) => FindOptions>, options?: AddScopeOptions ): void; @@ -1856,7 +1863,7 @@ export abstract class Model( this: ModelStatic, - options?: FindOptions): Promise; + options?: FindOptions>): Promise; /** * Search for a single instance by its primary key. This applies LIMIT 1, so the listener will @@ -1865,12 +1872,12 @@ export abstract class Model( this: ModelStatic, identifier: Identifier, - options: Omit, 'where'> + options: Omit>, 'where'> ): Promise; public static findByPk( this: ModelStatic, identifier?: Identifier, - options?: Omit, 'where'> + options?: Omit>, 'where'> ): Promise; /** @@ -1878,11 +1885,11 @@ export abstract class Model( this: ModelStatic, - options: NonNullFindOptions + options: NonNullFindOptions> ): Promise; public static findOne( this: ModelStatic, - options?: FindOptions + options?: FindOptions> ): Promise; /** @@ -1896,9 +1903,9 @@ export abstract class Model( this: ModelStatic, - field: keyof M['_attributes'] | '*', + field: keyof Attributes | '*', aggregateFunction: string, - options?: AggregateOptions + options?: AggregateOptions> ): Promise; /** @@ -1906,7 +1913,7 @@ export abstract class Model( this: ModelStatic, - options: CountWithOptions + options: CountWithOptions> ): Promise>; /** @@ -1916,7 +1923,7 @@ export abstract class Model( this: ModelStatic, - options?: CountOptions + options?: CountOptions> ): Promise; /** @@ -1956,11 +1963,11 @@ export abstract class Model( this: ModelStatic, - options?: Omit, 'group'> + options?: Omit>, 'group'> ): Promise<{ rows: M[]; count: number }>; public static findAndCountAll( this: ModelStatic, - options: SetRequired, 'group'> + options: SetRequired>, 'group'> ): Promise<{ rows: M[]; count: number[] }>; /** @@ -1968,8 +1975,8 @@ export abstract class Model( this: ModelStatic, - field: keyof M['_attributes'], - options?: AggregateOptions + field: keyof Attributes, + options?: AggregateOptions> ): Promise; /** @@ -1977,8 +1984,8 @@ export abstract class Model( this: ModelStatic, - field: keyof M['_attributes'], - options?: AggregateOptions + field: keyof Attributes, + options?: AggregateOptions> ): Promise; /** @@ -1986,8 +1993,8 @@ export abstract class Model( this: ModelStatic, - field: keyof M['_attributes'], - options?: AggregateOptions + field: keyof Attributes, + options?: AggregateOptions> ): Promise; /** @@ -1995,7 +2002,7 @@ export abstract class Model( this: ModelStatic, - record?: MakeUndefinedOptional, + record?: CreationAttributes, options?: BuildOptions ): M; @@ -2004,7 +2011,7 @@ export abstract class Model( this: ModelStatic, - records: ReadonlyArray>, + records: ReadonlyArray>, options?: BuildOptions ): M[]; @@ -2013,10 +2020,10 @@ export abstract class Model = CreateOptions + O extends CreateOptions> = CreateOptions> >( this: ModelStatic, - values?: MakeUndefinedOptional, + values?: CreationAttributes, options?: O ): Promise; @@ -2027,8 +2034,8 @@ export abstract class Model( this: ModelStatic, options: FindOrCreateOptions< - M['_attributes'], - MakeUndefinedOptional + Attributes, + CreationAttributes > ): Promise<[M, boolean]>; @@ -2045,7 +2052,7 @@ export abstract class Model( this: ModelStatic, - options: FindOrCreateOptions> + options: FindOrCreateOptions, CreationAttributes> ): Promise<[M, boolean]>; /** @@ -2054,7 +2061,7 @@ export abstract class Model( this: ModelStatic, - options: FindOrCreateOptions> + options: FindOrCreateOptions, CreationAttributes> ): Promise<[M, boolean]>; /** @@ -2078,8 +2085,8 @@ export abstract class Model( this: ModelStatic, - values: MakeUndefinedOptional, - options?: UpsertOptions + values: CreationAttributes, + options?: UpsertOptions> ): Promise<[M, boolean | null]>; /** @@ -2095,8 +2102,8 @@ export abstract class Model( this: ModelStatic, - records: ReadonlyArray>, - options?: BulkCreateOptions + records: ReadonlyArray>, + options?: BulkCreateOptions> ): Promise; /** @@ -2104,7 +2111,7 @@ export abstract class Model( this: ModelStatic, - options?: TruncateOptions + options?: TruncateOptions> ): Promise; /** @@ -2114,7 +2121,7 @@ export abstract class Model( this: ModelStatic, - options?: DestroyOptions + options?: DestroyOptions> ): Promise; /** @@ -2122,7 +2129,7 @@ export abstract class Model( this: ModelStatic, - options?: RestoreOptions + options?: RestoreOptions> ): Promise; /** @@ -2133,9 +2140,9 @@ export abstract class Model( this: ModelStatic, values: { - [key in keyof M['_attributes']]?: M['_attributes'][key] | Fn | Col | Literal; + [key in keyof Attributes]?: Attributes[key] | Fn | Col | Literal; }, - options: UpdateOptions + options: UpdateOptions> ): Promise<[number, M[]]>; /** @@ -2143,8 +2150,8 @@ export abstract class Model( this: ModelStatic, - field: keyof M['_attributes'], - options: IncrementDecrementOptionsWithBy + field: keyof Attributes, + options: IncrementDecrementOptionsWithBy> ): Promise; /** @@ -2152,8 +2159,8 @@ export abstract class Model( this: ModelStatic, - fields: ReadonlyArray, - options: IncrementDecrementOptionsWithBy + fields: ReadonlyArray>, + options: IncrementDecrementOptionsWithBy> ): Promise; /** @@ -2161,8 +2168,8 @@ export abstract class Model( this: ModelStatic, - fields: { [key in keyof M['_attributes']]?: number }, - options: IncrementDecrementOptions + fields: { [key in keyof Attributes]?: number }, + options: IncrementDecrementOptions> ): Promise; /** @@ -2170,8 +2177,8 @@ export abstract class Model( this: ModelStatic, - field: keyof M['_attributes'], - options: IncrementDecrementOptionsWithBy + field: keyof Attributes, + options: IncrementDecrementOptionsWithBy> ): Promise; /** @@ -2179,8 +2186,8 @@ export abstract class Model( this: ModelStatic, - fields: (keyof M['_attributes'])[], - options: IncrementDecrementOptionsWithBy + fields: (keyof Attributes)[], + options: IncrementDecrementOptionsWithBy> ): Promise; /** @@ -2188,8 +2195,8 @@ export abstract class Model( this: ModelStatic, - fields: { [key in keyof M['_attributes']]?: number }, - options: IncrementDecrementOptions + fields: { [key in keyof Attributes]?: number }, + options: IncrementDecrementOptions> ): Promise; /** @@ -2244,11 +2251,11 @@ export abstract class Model( this: ModelStatic, name: string, - fn: (instance: M, options: CreateOptions) => HookReturn + fn: (instance: M, options: CreateOptions>) => HookReturn ): void; public static beforeCreate( this: ModelStatic, - fn: (instance: M, options: CreateOptions) => HookReturn + fn: (instance: M, options: CreateOptions>) => HookReturn ): void; /** @@ -2260,11 +2267,11 @@ export abstract class Model( this: ModelStatic, name: string, - fn: (instance: M, options: CreateOptions) => HookReturn + fn: (instance: M, options: CreateOptions>) => HookReturn ): void; public static afterCreate( this: ModelStatic, - fn: (instance: M, options: CreateOptions) => HookReturn + fn: (instance: M, options: CreateOptions>) => HookReturn ): void; /** @@ -2308,11 +2315,11 @@ export abstract class Model( this: ModelStatic, name: string, - fn: (instance: M, options: UpdateOptions) => HookReturn + fn: (instance: M, options: UpdateOptions>) => HookReturn ): void; public static beforeUpdate( this: ModelStatic, - fn: (instance: M, options: UpdateOptions) => HookReturn + fn: (instance: M, options: UpdateOptions>) => HookReturn ): void; /** @@ -2324,11 +2331,11 @@ export abstract class Model( this: ModelStatic, name: string, - fn: (instance: M, options: UpdateOptions) => HookReturn + fn: (instance: M, options: UpdateOptions>) => HookReturn ): void; public static afterUpdate( this: ModelStatic, - fn: (instance: M, options: UpdateOptions) => HookReturn + fn: (instance: M, options: UpdateOptions>) => HookReturn ): void; /** @@ -2340,11 +2347,11 @@ export abstract class Model( this: ModelStatic, name: string, - fn: (instance: M, options: UpdateOptions | SaveOptions) => HookReturn + fn: (instance: M, options: UpdateOptions> | SaveOptions>) => HookReturn ): void; public static beforeSave( this: ModelStatic, - fn: (instance: M, options: UpdateOptions | SaveOptions) => HookReturn + fn: (instance: M, options: UpdateOptions> | SaveOptions>) => HookReturn ): void; /** @@ -2356,11 +2363,11 @@ export abstract class Model( this: ModelStatic, name: string, - fn: (instance: M, options: UpdateOptions | SaveOptions) => HookReturn + fn: (instance: M, options: UpdateOptions> | SaveOptions>) => HookReturn ): void; public static afterSave( this: ModelStatic, - fn: (instance: M, options: UpdateOptions | SaveOptions) => HookReturn + fn: (instance: M, options: UpdateOptions> | SaveOptions>) => HookReturn ): void; /** @@ -2372,11 +2379,11 @@ export abstract class Model( this: ModelStatic, name: string, - fn: (instances: M[], options: BulkCreateOptions) => HookReturn + fn: (instances: M[], options: BulkCreateOptions>) => HookReturn ): void; public static beforeBulkCreate( this: ModelStatic, - fn: (instances: M[], options: BulkCreateOptions) => HookReturn + fn: (instances: M[], options: BulkCreateOptions>) => HookReturn ): void; /** @@ -2388,11 +2395,11 @@ export abstract class Model( this: ModelStatic, name: string, - fn: (instances: readonly M[], options: BulkCreateOptions) => HookReturn + fn: (instances: readonly M[], options: BulkCreateOptions>) => HookReturn ): void; public static afterBulkCreate( this: ModelStatic, - fn: (instances: readonly M[], options: BulkCreateOptions) => HookReturn + fn: (instances: readonly M[], options: BulkCreateOptions>) => HookReturn ): void; /** @@ -2403,10 +2410,10 @@ export abstract class Model( this: ModelStatic, - name: string, fn: (options: BulkCreateOptions) => HookReturn): void; + name: string, fn: (options: BulkCreateOptions>) => HookReturn): void; public static beforeBulkDestroy( this: ModelStatic, - fn: (options: BulkCreateOptions) => HookReturn + fn: (options: BulkCreateOptions>) => HookReturn ): void; /** @@ -2417,11 +2424,11 @@ export abstract class Model( this: ModelStatic, - name: string, fn: (options: DestroyOptions) => HookReturn + name: string, fn: (options: DestroyOptions>) => HookReturn ): void; public static afterBulkDestroy( this: ModelStatic, - fn: (options: DestroyOptions) => HookReturn + fn: (options: DestroyOptions>) => HookReturn ): void; /** @@ -2432,11 +2439,11 @@ export abstract class Model( this: ModelStatic, - name: string, fn: (options: UpdateOptions) => HookReturn + name: string, fn: (options: UpdateOptions>) => HookReturn ): void; public static beforeBulkUpdate( this: ModelStatic, - fn: (options: UpdateOptions) => HookReturn + fn: (options: UpdateOptions>) => HookReturn ): void; /** @@ -2447,11 +2454,11 @@ export abstract class Model( this: ModelStatic, - name: string, fn: (options: UpdateOptions) => HookReturn + name: string, fn: (options: UpdateOptions>) => HookReturn ): void; public static afterBulkUpdate( this: ModelStatic, - fn: (options: UpdateOptions) => HookReturn + fn: (options: UpdateOptions>) => HookReturn ): void; /** @@ -2462,11 +2469,11 @@ export abstract class Model( this: ModelStatic, - name: string, fn: (options: FindOptions) => HookReturn + name: string, fn: (options: FindOptions>) => HookReturn ): void; public static beforeFind( this: ModelStatic, - fn: (options: FindOptions) => HookReturn + fn: (options: FindOptions>) => HookReturn ): void; /** @@ -2477,11 +2484,11 @@ export abstract class Model( this: ModelStatic, - name: string, fn: (options: CountOptions) => HookReturn + name: string, fn: (options: CountOptions>) => HookReturn ): void; public static beforeCount( this: ModelStatic, - fn: (options: CountOptions) => HookReturn + fn: (options: CountOptions>) => HookReturn ): void; /** @@ -2492,11 +2499,11 @@ export abstract class Model( this: ModelStatic, - name: string, fn: (options: FindOptions) => HookReturn + name: string, fn: (options: FindOptions>) => HookReturn ): void; public static beforeFindAfterExpandIncludeAll( this: ModelStatic, - fn: (options: FindOptions) => HookReturn + fn: (options: FindOptions>) => HookReturn ): void; /** @@ -2507,11 +2514,11 @@ export abstract class Model( this: ModelStatic, - name: string, fn: (options: FindOptions) => HookReturn + name: string, fn: (options: FindOptions>) => HookReturn ): void; public static beforeFindAfterOptions( this: ModelStatic, - fn: (options: FindOptions) => void + fn: (options: FindOptions>) => void ): HookReturn; /** @@ -2523,11 +2530,11 @@ export abstract class Model( this: ModelStatic, name: string, - fn: (instancesOrInstance: readonly M[] | M | null, options: FindOptions) => HookReturn + fn: (instancesOrInstance: readonly M[] | M | null, options: FindOptions>) => HookReturn ): void; public static afterFind( this: ModelStatic, - fn: (instancesOrInstance: readonly M[] | M | null, options: FindOptions) => HookReturn + fn: (instancesOrInstance: readonly M[] | M | null, options: FindOptions>) => HookReturn ): void; /** @@ -2943,7 +2950,7 @@ interface NonAttributeBrandedArray extends Array { /** * This is a Branded Type. * You can use it to tag fields from your class that are NOT attributes. - * They will be ignored by {@link AttributesOf} and {@link CreationAttributesOf} + * They will be ignored by {@link InferAttributes} and {@link InferCreationAttributes} */ export type NonAttribute = T extends Array // Arrays are a special case when branding. Both sides need to be an array, @@ -2952,14 +2959,14 @@ export type NonAttribute = T extends Array : T | { [NonAttributeBrand]: true }; // this MUST be a union or nothing will be assignable to this type. /** - * Option bag for {@link AttributesOf}. + * Option bag for {@link InferAttributes}. * * - omit: properties to not treat as Attributes. */ -type AttributesOfOptions = { omit?: Excluded }; +type InferAttributesOptions = { omit?: Excluded }; /** - * Utility type to extract Attributes of a given Model. + * Utility type to extract Attributes of a given Model class. * * It returns all instance properties defined in the Model, except: * - those inherited from Model (intermediate inheritance works), @@ -2972,7 +2979,7 @@ type AttributesOfOptions = { omit?: Excluded }; * * @example * // listed attributes will be 'id' & 'firstName'. - * class User extends Model> { + * class User extends Model> { * id: number; * firstName: string; * } @@ -2980,7 +2987,7 @@ type AttributesOfOptions = { omit?: Excluded }; * @example * // listed attributes will be 'id' & 'firstName'. * // we're excluding the `name` getter & `projects` attribute using the `omit` option. - * class User extends Model> { + * class User extends Model> { * id: number; * firstName: string; * @@ -2993,7 +3000,7 @@ type AttributesOfOptions = { omit?: Excluded }; * @example * // listed attributes will be 'id' & 'firstName'. * // we're excluding the `name` getter & `test` attribute using the `NonAttribute` branded type. - * class User extends Model> { + * class User extends Model> { * id: number; * firstName: string; * @@ -3003,9 +3010,9 @@ type AttributesOfOptions = { omit?: Excluded }; * projects?: NonAttribute; * } */ -export type AttributesOf< +export type InferAttributes< M extends Model, - Options extends AttributesOfOptions = { omit: never } + Options extends InferAttributesOptions = { omit: never } > = { [Key in keyof M as M[Key] extends AnyFunction ? never @@ -3033,7 +3040,7 @@ interface CreationAttributeBrandedArray extends Array { * This is a Branded Type. * You can use it to tag attributes that can be ommited during Model Creation. * - * For use with {@link CreationAttributesOf}. + * For use with {@link InferCreationAttributes}. */ export type CreationOptional = T extends Array // Arrays are a special case when branding. Both sides need to be an array, @@ -3042,13 +3049,13 @@ export type CreationOptional = T extends Array : T | { [CreationAttributeBrand]: true }; // this MUST be a union or nothing will be assignable to this type. /** - * Utility type to extract Attributes of a given Model. + * Utility type to extract Creation Attributes of a given Model class. * - * Works like {@link AttributesOf}, but fields that are tagged using + * Works like {@link InferAttributes}, but fields that are tagged using * {@link CreationOptional} will be optional. * * @example - * class User extends Model, CreationAttributesOf> { + * class User extends Model, InferCreationAttributes> { * // this attribute is optional in Model#create * declare id: CreationOptional; * @@ -3056,9 +3063,9 @@ export type CreationOptional = T extends Array * declare name: string; * } */ -export type CreationAttributesOf< +export type InferCreationAttributes< M extends Model, - Options extends AttributesOfOptions = { omit: never } + Options extends InferAttributesOptions = { omit: never } > = { [Key in keyof M as M[Key] extends AnyFunction ? never @@ -3076,3 +3083,27 @@ export type CreationAttributesOf< : CreationAttributeBrandedArray extends M[Key] ? (M[Key] | undefined) : M[Key] }; + +// in v7, we should be able to drop InferCreationAttributes and InferAttributes, +// resolving this confusion. +/** + * Returns the creation attributes of a given Model. + * + * This returns the Creation Attributes of a Model, it does not build them. + * If you need to build them, use {@link InferCreationAttributes}. + * + * @example + * function buildModel(modelClass: ModelStatic, attributes: CreationAttributes) {} + */ +export type CreationAttributes = MakeUndefinedOptional; + +/** + * Returns the creation attributes of a given Model. + * + * This returns the Attributes of a Model that have already been defined, it does not build them. + * If you need to build them, use {@link InferAttributes}. + * + * @example + * function getValue(modelClass: ModelStatic, attribute: keyof Attributes) {} + */ +export type Attributes = M['_attributes']; diff --git a/types/lib/query-interface.d.ts b/types/lib/query-interface.d.ts index 0f230c5df138..274d3048aea3 100644 --- a/types/lib/query-interface.d.ts +++ b/types/lib/query-interface.d.ts @@ -8,7 +8,11 @@ import { WhereOptions, Filterable, Poolable, - ModelCtor, ModelStatic, ModelType + ModelCtor, + ModelStatic, + ModelType, + CreationAttributes, + Attributes, } from './model'; import QueryTypes = require('./query-types'); import { Sequelize, RetryOptions } from './sequelize'; @@ -337,7 +341,7 @@ export class QueryInterface { */ public createTable( tableName: TableName, - attributes: ModelAttributes, + attributes: ModelAttributes>, options?: QueryInterfaceCreateTableOptions ): Promise; @@ -508,7 +512,7 @@ export class QueryInterface { instance: M, tableName: TableName, values: object, - identifier: WhereOptions, + identifier: WhereOptions>, options?: QueryOptions ): Promise; @@ -555,7 +559,7 @@ export class QueryInterface { instance: Model, tableName: TableName, values: object, - identifier: WhereOptions, + identifier: WhereOptions>, options?: QueryOptions ): Promise; diff --git a/types/lib/sequelize.d.ts b/types/lib/sequelize.d.ts index a4b91b01bc1c..ee9af4e5c9d3 100644 --- a/types/lib/sequelize.d.ts +++ b/types/lib/sequelize.d.ts @@ -20,7 +20,7 @@ import { WhereOperators, ModelCtor, Hookable, - ModelType, + ModelType, CreationAttributes, Attributes, } from './model'; import { ModelManager } from './model-manager'; import { QueryInterface, QueryOptions, QueryOptionsWithModel, QueryOptionsWithType, ColumnsDescription } from './query-interface'; @@ -752,10 +752,10 @@ export class Sequelize extends Hooks { */ public static beforeDefine( name: string, - fn: (attributes: ModelAttributes, options: ModelOptions) => void + fn: (attributes: ModelAttributes>, options: ModelOptions) => void ): void; public static beforeDefine( - fn: (attributes: ModelAttributes, options: ModelOptions) => void + fn: (attributes: ModelAttributes>, options: ModelOptions) => void ): void; /** @@ -1181,7 +1181,7 @@ export class Sequelize extends Hooks { * @param options These options are merged with the default define options provided to the Sequelize * constructor */ - public define( + public define>( modelName: string, attributes: ModelAttributes, options?: ModelOptions diff --git a/types/lib/utils.d.ts b/types/lib/utils.d.ts index 89007e46f1a2..de1ee23dca2e 100644 --- a/types/lib/utils.d.ts +++ b/types/lib/utils.d.ts @@ -1,5 +1,5 @@ import { DataType } from './data-types'; -import { Model, ModelCtor, ModelType, WhereOptions } from './model'; +import { Model, ModelCtor, ModelType, WhereOptions, Attributes } from './model'; import { Optional } from '..'; export type Primitive = 'string' | 'number' | 'boolean'; @@ -33,13 +33,13 @@ export interface OptionsForMapping { } /** Expand and normalize finder options */ -export function mapFinderOptions>( +export function mapFinderOptions>>( options: T, model: ModelCtor ): T; /* Used to map field names in attributes and where conditions */ -export function mapOptionFieldNames>( +export function mapOptionFieldNames>>( options: T, model: ModelCtor ): T; diff --git a/types/test/attributes-of.ts b/types/test/infer-attributes.ts similarity index 76% rename from types/test/attributes-of.ts rename to types/test/infer-attributes.ts index fdd570fb5d03..1892acb5b6aa 100644 --- a/types/test/attributes-of.ts +++ b/types/test/infer-attributes.ts @@ -1,10 +1,10 @@ import { expectTypeOf } from 'expect-type'; -import { AttributesOf, CreationAttributesOf, CreationOptional, Model, NonAttribute } from 'sequelize'; +import { InferAttributes, InferCreationAttributes, CreationOptional, Model, NonAttribute, Attributes, CreationAttributes } from 'sequelize'; -type UserAttributes = AttributesOf; -type UserCreationAttributes = CreationAttributesOf; - -class User extends Model { +class User extends Model< + InferAttributes, + InferCreationAttributes +> { declare id: CreationOptional; declare name: string; declare anArray: CreationOptional; @@ -18,14 +18,17 @@ class User extends Model { static staticMethod() {} } +type UserAttributes = Attributes; +type UserCreationAttributes = CreationAttributes; + expectTypeOf().not.toBeAny(); { - class Test extends Model> { + class Test extends Model> { declare id: NonAttribute; } - const win: Test['_attributes'] = {}; + const win: Attributes = {}; } { @@ -70,7 +73,7 @@ expectTypeOf().not.toBeAny(); }; } -type GroupAttributes = AttributesOf; +type GroupAttributes = InferAttributes; class Group extends Model { declare id: number; @@ -81,13 +84,13 @@ class Group extends Model { const fail1: GroupAttributes = {}; // @ts-expect-error - id should not be missing - const fail2: AttributesOf = {}; + const fail2: InferAttributes = {}; // @ts-expect-error - id should not be missing - const fail3: AttributesOf = {}; + const fail3: InferAttributes = {}; } -class Project extends Model> { +class Project extends Model> { declare id: number; } diff --git a/types/test/models/User.ts b/types/test/models/User.ts index ba6e630bd6d8..e2029fb062f7 100644 --- a/types/test/models/User.ts +++ b/types/test/models/User.ts @@ -1,9 +1,11 @@ import { - AttributesOf, + InferAttributes, BelongsTo, BelongsToCreateAssociationMixin, BelongsToGetAssociationMixin, - BelongsToSetAssociationMixin, CreationAttributesOf, CreationOptional, + BelongsToSetAssociationMixin, + InferCreationAttributes, + CreationOptional, DataTypes, FindOptions, Model, @@ -15,8 +17,8 @@ import { sequelize } from '../connection'; type NonUserAttributes = 'group'; export class User extends Model< - AttributesOf, - CreationAttributesOf + InferAttributes, + InferCreationAttributes > { public static associations: { group: BelongsTo; @@ -103,7 +105,7 @@ User.afterFind((users, options) => { }); // TODO: VSCode shows the typing being correctly narrowed but doesn't do it correctly -User.addHook('beforeFind', 'test', (options: FindOptions>) => { +User.addHook('beforeFind', 'test', (options: FindOptions>) => { return undefined; }); diff --git a/types/test/models/UserGroup.ts b/types/test/models/UserGroup.ts index 40451ca29eb0..e50d4ac6eec9 100644 --- a/types/test/models/UserGroup.ts +++ b/types/test/models/UserGroup.ts @@ -1,5 +1,7 @@ import { - AttributesOf, CreationAttributesOf, CreationOptional, + InferAttributes, + InferCreationAttributes, + CreationOptional, DataTypes, HasMany, HasManyAddAssociationMixin, @@ -21,8 +23,8 @@ import { User } from './User'; // This class doesn't extend the generic Model, but should still // function just fine, with a bit less safe type-checking export class UserGroup extends Model< - AttributesOf, - CreationAttributesOf + InferAttributes, + InferCreationAttributes > { public static associations: { users: HasMany diff --git a/types/test/typescriptDocs/ModelInit.ts b/types/test/typescriptDocs/ModelInit.ts index 56e104fdf69b..134bf80a9b82 100644 --- a/types/test/typescriptDocs/ModelInit.ts +++ b/types/test/typescriptDocs/ModelInit.ts @@ -7,13 +7,13 @@ import { Association, DataTypes, HasManyAddAssociationMixin, HasManyCountAssociationsMixin, HasManyCreateAssociationMixin, HasManyGetAssociationsMixin, HasManyHasAssociationMixin, Model, - ModelDefined, Optional, Sequelize, AttributesOf, CreationAttributesOf, CreationOptional, NonAttribute + ModelDefined, Optional, Sequelize, InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute } from 'sequelize'; const sequelize = new Sequelize('mysql://root:asd123@localhost:3306/mydb'); // 'projects' is excluded as it's not an attribute, it's an association. -class User extends Model, CreationAttributesOf> { +class User extends Model, InferCreationAttributes> { // id can be undefined during creation when using `autoIncrement` declare id: CreationOptional; declare name: string; @@ -49,11 +49,9 @@ class User extends Model, CreationAttri }; } -// You can write `extends Model, AttributesOf>` instead, -// but that will do the exact same thing as below class Project extends Model< - AttributesOf, - CreationAttributesOf + InferAttributes, + InferCreationAttributes > { // id can be undefined during creation when using `autoIncrement` declare id: CreationOptional; @@ -71,8 +69,8 @@ class Project extends Model< } class Address extends Model< - AttributesOf
, - CreationAttributesOf
+ InferAttributes
, + InferCreationAttributes
> { declare userId: number; declare address: string; From 1da9ce3ad5e7e5e958385e16f56567b4faca0373 Mon Sep 17 00:00:00 2001 From: ephys Date: Sat, 8 Jan 2022 14:53:22 +0100 Subject: [PATCH 15/21] docs(typescript): add missing methods / remove readonly --- docs/manual/other-topics/typescript.md | 25 ++++++++++++++++--------- types/test/typescriptDocs/ModelInit.ts | 25 ++++++++++++++++--------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/docs/manual/other-topics/typescript.md b/docs/manual/other-topics/typescript.md index 09dbace4254d..56d0c261e327 100644 --- a/docs/manual/other-topics/typescript.md +++ b/docs/manual/other-topics/typescript.md @@ -82,8 +82,10 @@ Example of a minimal TypeScript project with strict type-checking for attributes ```typescript import { Association, DataTypes, HasManyAddAssociationMixin, HasManyCountAssociationsMixin, - HasManyCreateAssociationMixin, HasManyGetAssociationsMixin, HasManyHasAssociationMixin, Model, - ModelDefined, Optional, Sequelize, InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute + HasManyCreateAssociationMixin, HasManyGetAssociationsMixin, HasManyHasAssociationMixin, + HasManySetAssociationsMixin, HasManyAddAssociationsMixin, HasManyHasAssociationsMixin, + HasManyRemoveAssociationMixin, HasManyRemoveAssociationsMixin, Model, ModelDefined, Optional, + Sequelize, InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute } from 'sequelize'; const sequelize = new Sequelize('mysql://root:asd123@localhost:3306/mydb'); @@ -97,22 +99,27 @@ class User extends Model, InferCreat // timestamps! // createdAt can be undefined during creation - declare readonly createdAt: CreationOptional; + declare createdAt: CreationOptional; // updatedAt can be undefined during creation - declare readonly updatedAt: CreationOptional; + declare updatedAt: CreationOptional; // 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; // Note the null assertions! declare addProject: HasManyAddAssociationMixin; + declare addProjects: HasManyAddAssociationsMixin; + declare setProjects: HasManySetAssociationsMixin; + declare removeProject: HasManyRemoveAssociationMixin; + declare removeProjects: HasManyRemoveAssociationsMixin; declare hasProject: HasManyHasAssociationMixin; + declare hasProjects: HasManyHasAssociationsMixin; declare countProjects: HasManyCountAssociationsMixin; declare createProject: HasManyCreateAssociationMixin; // You can also pre-declare possible inclusions, these will only be populated if you // actively include a relation. - declare readonly projects?: NonAttribute; // Note this is optional since it's only populated when explicitly requested in code + declare projects?: NonAttribute; // 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. @@ -139,9 +146,9 @@ class Project extends Model< declare owner?: NonAttribute; // createdAt can be undefined during creation - declare readonly createdAt: CreationOptional; + declare createdAt: CreationOptional; // updatedAt can be undefined during creation - declare readonly updatedAt: CreationOptional; + declare updatedAt: CreationOptional; } class Address extends Model< @@ -152,9 +159,9 @@ class Address extends Model< declare address: string; // createdAt can be undefined during creation - declare readonly createdAt: CreationOptional; + declare createdAt: CreationOptional; // updatedAt can be undefined during creation - declare readonly updatedAt: CreationOptional; + declare updatedAt: CreationOptional; } Project.init( diff --git a/types/test/typescriptDocs/ModelInit.ts b/types/test/typescriptDocs/ModelInit.ts index 134bf80a9b82..ea7ebe6cd6e3 100644 --- a/types/test/typescriptDocs/ModelInit.ts +++ b/types/test/typescriptDocs/ModelInit.ts @@ -6,8 +6,10 @@ */ import { Association, DataTypes, HasManyAddAssociationMixin, HasManyCountAssociationsMixin, - HasManyCreateAssociationMixin, HasManyGetAssociationsMixin, HasManyHasAssociationMixin, Model, - ModelDefined, Optional, Sequelize, InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute + HasManyCreateAssociationMixin, HasManyGetAssociationsMixin, HasManyHasAssociationMixin, + HasManySetAssociationsMixin, HasManyAddAssociationsMixin, HasManyHasAssociationsMixin, + HasManyRemoveAssociationMixin, HasManyRemoveAssociationsMixin, Model, ModelDefined, Optional, + Sequelize, InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute } from 'sequelize'; const sequelize = new Sequelize('mysql://root:asd123@localhost:3306/mydb'); @@ -21,22 +23,27 @@ class User extends Model, InferCreat // timestamps! // createdAt can be undefined during creation - declare readonly createdAt: CreationOptional; + declare createdAt: CreationOptional; // updatedAt can be undefined during creation - declare readonly updatedAt: CreationOptional; + declare updatedAt: CreationOptional; // 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; // Note the null assertions! declare addProject: HasManyAddAssociationMixin; + declare addProjects: HasManyAddAssociationsMixin; + declare setProjects: HasManySetAssociationsMixin; + declare removeProject: HasManyRemoveAssociationMixin; + declare removeProjects: HasManyRemoveAssociationsMixin; declare hasProject: HasManyHasAssociationMixin; + declare hasProjects: HasManyHasAssociationsMixin; declare countProjects: HasManyCountAssociationsMixin; declare createProject: HasManyCreateAssociationMixin; // You can also pre-declare possible inclusions, these will only be populated if you // actively include a relation. - declare readonly projects?: NonAttribute; // Note this is optional since it's only populated when explicitly requested in code + declare projects?: NonAttribute; // 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. @@ -63,9 +70,9 @@ class Project extends Model< declare owner?: NonAttribute; // createdAt can be undefined during creation - declare readonly createdAt: CreationOptional; + declare createdAt: CreationOptional; // updatedAt can be undefined during creation - declare readonly updatedAt: CreationOptional; + declare updatedAt: CreationOptional; } class Address extends Model< @@ -76,9 +83,9 @@ class Address extends Model< declare address: string; // createdAt can be undefined during creation - declare readonly createdAt: CreationOptional; + declare createdAt: CreationOptional; // updatedAt can be undefined during creation - declare readonly updatedAt: CreationOptional; + declare updatedAt: CreationOptional; } Project.init( From 644afbea9caca2adfcd7d7681c8c01af13d2b1df Mon Sep 17 00:00:00 2001 From: ephys Date: Sun, 16 Jan 2022 12:38:53 +0100 Subject: [PATCH 16/21] refactor: add unused catch binding to workaround esbuild issue --- lib/sequelize.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sequelize.js b/lib/sequelize.js index c8ad42189220..d89db5e3f16c 100644 --- a/lib/sequelize.js +++ b/lib/sequelize.js @@ -232,7 +232,7 @@ class Sequelize { try { const o = JSON.parse(urlParts.query.options); options.dialectOptions.options = o; - } catch { + } catch (error) { // eslint-disable-line unicorn/prefer-optional-catch-binding -- esdoc hasn't been updated and doesn't support this. Remove once https://github.com/sequelize/sequelize/pull/13914 is merged // Nothing to do, string is not a valid JSON // an thus does not need any further processing } From 7c908e3cc46e1cbc7a5f2d4481f3baacee6da4f2 Mon Sep 17 00:00:00 2001 From: ephys Date: Sun, 16 Jan 2022 12:56:16 +0100 Subject: [PATCH 17/21] refactor: add unused catch binding to workaround esbuild issue --- lib/sequelize.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sequelize.js b/lib/sequelize.js index d89db5e3f16c..c6b08ab69897 100644 --- a/lib/sequelize.js +++ b/lib/sequelize.js @@ -1134,7 +1134,7 @@ class Sequelize { // release the connection, even if we don't need to rollback await transaction.cleanup(); } - } catch { + } catch (error_) { // eslint-disable-line unicorn/prefer-optional-catch-binding -- esdoc hasn't been updated and doesn't support this. Remove once https://github.com/sequelize/sequelize/pull/13914 is merged // ignore } From 7b97c0d0bb78b93d3d57867bc4c63ed9de99541d Mon Sep 17 00:00:00 2001 From: ephys Date: Sun, 16 Jan 2022 15:08:14 +0100 Subject: [PATCH 18/21] refactor: make esdocs able to compile typescript.md --- docs/manual/other-topics/typescript.md | 11 ++++------- lib/sequelize.js | 4 ++-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/docs/manual/other-topics/typescript.md b/docs/manual/other-topics/typescript.md index 6ca9fe639126..290f72785746 100644 --- a/docs/manual/other-topics/typescript.md +++ b/docs/manual/other-topics/typescript.md @@ -80,7 +80,7 @@ You only need to use `CreationOptional` & `NonAttribute` on class instance field Example of a minimal TypeScript project with strict type-checking for attributes: - +[//]: # (NOTE for maintainers: Keep the following code in sync with `/types/test/typescriptDocs/ModelInit.ts` to ensure it typechecks correctly.) ```typescript import { @@ -310,8 +310,7 @@ async function doStuffWithUser() { The typings for Sequelize v5 allowed you to define models without specifying types for the attributes. This is still possible for backwards compatibility and for cases where you feel strict typing for attributes isn't worth it. - +[//]: # (NOTE for maintainers: Keep the following code in sync with `typescriptDocs/ModelInitNoAttributes.ts` to ensure it typechecks correctly.) ```ts import { Sequelize, Model, DataTypes } from "sequelize"; @@ -363,8 +362,7 @@ async function doStuffWithUserModel() { In Sequelize versions before v5, the default way of defining a model involved using `sequelize.define`. It's still possible to define models with that, and you can also add typings to these models using interfaces. - +[//]: # (NOTE for maintainers: Keep the following code in sync with `typescriptDocs/Define.ts` to ensure it typechecks correctly.) ```ts import { Sequelize, Model, DataTypes, Optional } from "sequelize"; @@ -405,8 +403,7 @@ async function doStuff() { If you're comfortable with somewhat less strict typing for the attributes on a model, you can save some code by defining the Instance to just extend `Model` without any attributes in the generic types. - +[//]: # (NOTE for maintainers: Keep the following code in sync with `typescriptDocs/DefineNoAttributes.ts` to ensure it typechecks correctly.) ```ts import { Sequelize, Model, DataTypes } from "sequelize"; diff --git a/lib/sequelize.js b/lib/sequelize.js index c6b08ab69897..c8ad42189220 100644 --- a/lib/sequelize.js +++ b/lib/sequelize.js @@ -232,7 +232,7 @@ class Sequelize { try { const o = JSON.parse(urlParts.query.options); options.dialectOptions.options = o; - } catch (error) { // eslint-disable-line unicorn/prefer-optional-catch-binding -- esdoc hasn't been updated and doesn't support this. Remove once https://github.com/sequelize/sequelize/pull/13914 is merged + } catch { // Nothing to do, string is not a valid JSON // an thus does not need any further processing } @@ -1134,7 +1134,7 @@ class Sequelize { // release the connection, even if we don't need to rollback await transaction.cleanup(); } - } catch (error_) { // eslint-disable-line unicorn/prefer-optional-catch-binding -- esdoc hasn't been updated and doesn't support this. Remove once https://github.com/sequelize/sequelize/pull/13914 is merged + } catch { // ignore } From 853926c44446a1159e7af192ca65115fdc507b6a Mon Sep 17 00:00:00 2001 From: ephys Date: Mon, 17 Jan 2022 09:27:18 +0100 Subject: [PATCH 19/21] refactor: implement review changes --- types/lib/associations/belongs-to-many.d.ts | 3 ++- types/lib/associations/has-many.d.ts | 4 ++-- types/lib/hooks.d.ts | 4 ++-- types/lib/model.d.ts | 4 ++-- types/lib/sequelize.d.ts | 4 +++- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/types/lib/associations/belongs-to-many.d.ts b/types/lib/associations/belongs-to-many.d.ts index 7852bc081a2b..af3e3075cc86 100644 --- a/types/lib/associations/belongs-to-many.d.ts +++ b/types/lib/associations/belongs-to-many.d.ts @@ -1,6 +1,7 @@ import { BulkCreateOptions, - CreateOptions, CreationAttributes, + CreateOptions, + CreationAttributes, Filterable, FindAttributeOptions, FindOptions, diff --git a/types/lib/associations/has-many.d.ts b/types/lib/associations/has-many.d.ts index 7b8903131933..ee83127b845a 100644 --- a/types/lib/associations/has-many.d.ts +++ b/types/lib/associations/has-many.d.ts @@ -1,6 +1,7 @@ import { DataType } from '../data-types'; import { - CreateOptions, CreationAttributes, + CreateOptions, + CreationAttributes, Filterable, FindOptions, InstanceUpdateOptions, @@ -9,7 +10,6 @@ import { Transactionable, } from '../model'; import { Association, ManyToManyOptions, MultiAssociationAccessors } from './base'; -import { MakeUndefinedOptional } from '../utils'; /** * Options provided when associating models with hasMany relationship diff --git a/types/lib/hooks.d.ts b/types/lib/hooks.d.ts index 53a5761d703f..b9620f172a64 100644 --- a/types/lib/hooks.d.ts +++ b/types/lib/hooks.d.ts @@ -101,7 +101,7 @@ export class Hooks< * @deprecated This property will become a Symbol in v7 to prevent collisions. * Use Attributes instead of this property to be forward-compatible. */ - _attributes: TModelAttributes; + _attributes: TModelAttributes; // TODO [>6]: make this a non-exported symbol (same as the one in model.d.ts) /** * A similar dummy variable that doesn't exist on the real object. Do not * try to access this in real code. @@ -109,7 +109,7 @@ export class Hooks< * @deprecated This property will become a Symbol in v7 to prevent collisions. * Use CreationAttributes instead of this property to be forward-compatible. */ - _creationAttributes: TCreationAttributes; + _creationAttributes: TCreationAttributes; // TODO [>6]: make this a non-exported symbol (same as the one in model.d.ts) /** * Add a hook to the model diff --git a/types/lib/model.d.ts b/types/lib/model.d.ts index defc3050bb09..b5f5982dbf92 100644 --- a/types/lib/model.d.ts +++ b/types/lib/model.d.ts @@ -1580,7 +1580,7 @@ export abstract class Model instead of this property to be forward-compatible. */ - _attributes: TModelAttributes; + _attributes: TModelAttributes; // TODO [>6]: make this a non-exported symbol (same as the one in hooks.d.ts) /** * A similar dummy variable that doesn't exist on the real object. Do not @@ -1589,7 +1589,7 @@ export abstract class Model instead of this property to be forward-compatible. */ - _creationAttributes: TCreationAttributes; + _creationAttributes: TCreationAttributes; // TODO [>6]: make this a non-exported symbol (same as the one in hooks.d.ts) /** The name of the database table */ public static readonly tableName: string; diff --git a/types/lib/sequelize.d.ts b/types/lib/sequelize.d.ts index ada7c71e00f5..dec11107fabb 100644 --- a/types/lib/sequelize.d.ts +++ b/types/lib/sequelize.d.ts @@ -19,7 +19,9 @@ import { WhereOperators, ModelCtor, Hookable, - ModelType, CreationAttributes, Attributes, + ModelType, + CreationAttributes, + Attributes, } from './model'; import { ModelManager } from './model-manager'; import { QueryInterface, QueryOptions, QueryOptionsWithModel, QueryOptionsWithType, ColumnsDescription } from './query-interface'; From e518b01426efaf2006e708b0685a4632e0b5d63f Mon Sep 17 00:00:00 2001 From: ephys Date: Fri, 21 Jan 2022 11:40:05 +0100 Subject: [PATCH 20/21] docs: specify InferAttributes release version + fix type names --- docs/manual/other-topics/typescript.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/manual/other-topics/typescript.md b/docs/manual/other-topics/typescript.md index 290f72785746..c43ad30e13ad 100644 --- a/docs/manual/other-topics/typescript.md +++ b/docs/manual/other-topics/typescript.md @@ -44,15 +44,15 @@ class User extends Model { } ``` -This solution is verbose. Sequelize v6 provides new utility types that will drastically reduce the amount -of boilerplate necessary: `AttributeOf`, and `CreationAttributeOf`. They will extract Attribute typings +This solution is verbose. Sequelize >=6.14.0 provides new utility types that will drastically reduce the amount +of boilerplate necessary: `InferAttributes`, and `InferCreationAttributes`. They will extract Attribute typings directly from the Model: ```typescript -import { Model, AttributesOf, CreationAttributesOf, CreationOptional } from 'sequelize'; +import { Model, InferAttributes, InferCreationAttributes, CreationOptional } from 'sequelize'; -// order of AttributesOf & CreationAttributesOf is important. -class User extends Model, CreationAttributesOf> { +// order of InferAttributes & InferCreationAttributes is important. +class User extends Model, InferCreationAttributes> { // 'CreationOptional' is a special type that marks the field as optional // when creating an instance of the model (such as using Model.create()). declare id: CreationOptional; @@ -61,19 +61,19 @@ class User extends Model, CreationAttributesOf> { } ``` -Important things to know about `AttributesOf` & `CreationAttributes` work: They will select all declared properties of the class except: +Important things to know about `InferAttributes` & `InferCreationAttributes` work: They will select all declared properties of the class except: - Static fields and methods. - Methods (anything whose type is a function). - Those whose type uses the branded type `NonAttribute`. -- Those excluded by using AttributesOf like this: `AttributesOf`. +- Those excluded by using AttributesOf like this: `InferAttributes`. - Those declared by the Model superclass (but not intermediary classes!). If one of your attributes shares the same name as one of the properties of `Model`, change its name. Doing this is likely to cause issues anyway. - Getter & setters are not automatically excluded. Set their return / parameter type to `NonAttribute`, or add them to `omit` to exclude them. -`CreationAttributes` works the same way as `AttributesOf` with one exception: Properties typed using the `CreationOptional` type +`InferCreationAttributes` works the same way as `AttributesOf` with one exception: Properties typed using the `CreationOptional` type will be marked as optional. You only need to use `CreationOptional` & `NonAttribute` on class instance fields or getters. From 2706b51df5a4e8a2604744aaaac5c7cdaf089a85 Mon Sep 17 00:00:00 2001 From: ephys Date: Fri, 21 Jan 2022 11:40:15 +0100 Subject: [PATCH 21/21] docs: document TypeScript utility types --- docs/manual/other-topics/typescript.md | 94 ++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/docs/manual/other-topics/typescript.md b/docs/manual/other-topics/typescript.md index c43ad30e13ad..cde929b3dee2 100644 --- a/docs/manual/other-topics/typescript.md +++ b/docs/manual/other-topics/typescript.md @@ -433,3 +433,97 @@ async function doStuff() { console.log(instance.id); } ``` + +## Utility Types + +### Requesting a Model Class + +`ModelStatic` is designed to be used to type a Model *class*. + +Here is an example of a utility method that requests a Model Class, and returns the list of primary keys defined in that class: + +```typescript +import { ModelStatic, ModelAttributeColumnOptions, Model, InferAttributes, InferCreationAttributes, CreationOptional } from 'sequelize'; + +/** + * Returns the list of attributes that are part of the model's primary key. + */ +export function getPrimaryKeyAttributes(model: ModelStatic): ModelAttributeColumnOptions[] { + const attributes: ModelAttributeColumnOptions[] = []; + + for (const attribute of Object.values(model.rawAttributes)) { + if (attribute.primaryKey) { + attributes.push(attribute); + } + } + + return attributes; +} + +class User extends Model, InferCreationAttributes> { + id: CreationOptional; +} + +User.init({ + id: { + type: DataTypes.INTEGER.UNSIGNED, + autoIncrement: true, + primaryKey: true + }, +}, { sequelize }); + +const primaryAttributes = getPrimaryKeyAttributes(User); +``` + +### Getting a Model's attributes + +If you need to access the list of attributes of a given model, `Attributes` and `CreationAttributes` +are what you need to use. + +They will return the Attributes (and Creation Attributes) of the Model passed as a parameter. + +Don't confuse them with `InferAttributes` and `InferCreationAttributes`. These two utility types should only every be used +in the definition of a Model to automatically create the list of attributes from the model's public class fields. They only work +with class-based model definitions (When using `Model.init`). + +`Attributes` and `CreationAttributes` will return the list of attributes of any model, no matter how they were created (be it `Model.init` or `Sequelize#define`). + +Here is an example of a utility function that requests a Model Class, and the name of an attribute ; and returns the corresponding attribute metadata. + +```typescript +import { + ModelStatic, + ModelAttributeColumnOptions, + Model, + InferAttributes, + InferCreationAttributes, + CreationOptional, + Attributes +} from 'sequelize'; + +export function getAttributeMetadata(model: ModelStatic, attributeName: keyof Attributes): ModelAttributeColumnOptions { + const attribute = model.rawAttributes[attributeName]; + if (attribute == null) { + throw new Error(`Attribute ${attributeName} does not exist on model ${model.name}`); + } + + return attribute; +} + +class User extends Model, InferCreationAttributes> { + id: CreationOptional; +} + +User.init({ + id: { + type: DataTypes.INTEGER.UNSIGNED, + autoIncrement: true, + primaryKey: true + }, +}, { sequelize }); + +const idAttributeMeta = getAttributeMetadata(User, 'id'); // works! + +// @ts-expect-error +const nameAttributeMeta = getAttributeMetadata(User, 'name'); // fails because 'name' is not an attribute of User +```