New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(types): added optional stricter typing for Model attributes #12405
Conversation
a39d6f5
to
007e975
Compare
Codecov Report
@@ Coverage Diff @@
## master #12405 +/- ##
==========================================
+ Coverage 90.28% 96.25% +5.96%
==========================================
Files 92 95 +3
Lines 8968 9308 +340
==========================================
+ Hits 8097 8959 +862
+ Misses 871 349 -522
Continue to review full report at Codecov.
|
007e975
to
3e51c6b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few comments about why I changed specific things -
@@ -5,7 +5,7 @@ import { OptimisticLockError } from '../lib/errors'; | |||
|
|||
async function test() { | |||
try { | |||
await User.create({ username: 'john_doe' }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made firstName
required in the attributes I defined, so I updated this code to match
types/lib/model.d.ts
Outdated
@@ -685,28 +683,28 @@ export interface Hookable { | |||
/** | |||
* Options for Model.findOrCreate method | |||
*/ | |||
export interface FindOrCreateOptions extends Filterable, Logging, Transactionable { | |||
export interface FindOrCreateOptions<TAttributes, TCreationAttributes> extends FindOptions<TAttributes> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made this code just extend FindOptions as other options (like attributes
) actually are accepted too, per the code inside model.js
which just directly does a findOne
with the options.
7821152
to
ad54f72
Compare
ad54f72
to
b3aaa8d
Compare
b3aaa8d
to
e4baea1
Compare
I made some fixes to the documentation and this is now ready for review! Edit: There's still some problems getting this to work both for |
Yes make those generic parameters optional, we should avoid breaking change if we can.
No problem with exporting this helper type |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
values?: object, | ||
options?: CreateOptions | ||
this: ModelStatic<M>, | ||
values?: M['_creationAttributes'], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is possible to create record with id
i.e. primary key. Distinction between CreationAttributes
and Attributes
from user point of view make sense, but internally all attributes are accepted.
We can also think about accepting TAttributes
generic which contains all attributes, then create a dynamic type for "CreationAttributes" by making primary key attribute optional in that type
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an interesting idea! The problem is - how do we figure out what's the primary key? We'd probably need users to provide it in a new generic (maybe TPrimaryKey
) but then that's just one more thing to provide. And then, there are still fields with defaultValue
which users can also omit, and other cases we'd need to handle.
I think TCreationAttributes
is a pretty flexible solution that solves for both primary keys and other columns with default values that can be omitted. It's also been used for the previous sequelize v4 typings, so would be familiar to those users.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, there is no good way to infer primary key in current Typescript schema. Actual attributes are passed to init
or define
, which contains this info.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think defining it in init
is especially problematic, so I recommend just leaving it as-is.
I just pushed a new change that made all types optional. I reverted some of the changes adding explicit Note that I did leave in the explicit |
🎉 This PR is included in version 6.2.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Ideally, we'd be able to extend multiple classes and be able to do something like this below. But Typescript does not have multiple inheritance: class UserAttributes {
public id!: number;
public username!: string;
public firstName!: string;
public lastName!: string;
public groupId!: number;
}
export class User extends Model<UserAttributes, UserCreationAttributes>, UserAttributes {
public static associations: {
group: BelongsTo<User, UserGroup>;
};
public createdAt!: Date;
public updatedAt!: Date;
// mixins for association (optional)
public group?: UserGroup;
public getGroup!: BelongsToGetAssociationMixin<UserGroup>;
public setGroup!: BelongsToSetAssociationMixin<UserGroup, number>;
public createGroup!: BelongsToCreateAssociationMixin<UserGroup>;
} This isn't possible because Typescript doesn't support multiple inheritance, but you might be able to achieve something similar with Mixins - https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#putting-all-of-the-above-rules-together-in-an-example Try it out! If you find a good solution, it'd be cool to post below and maybe even to incorporate it into the typings for the library. |
I can't find a way to make this work when doing a create with include. e.g.: class User extends Model<UserAttributes, UserCreationAttributes> {...}
class Project extends Model<ProjectAttributes, ProjectCreationAttributes> {...}
Project.hasMany(User);
Project.create({
users: [{ name: 'project owner' }],
}, { include: [User] }); I get the error: "Object literal may only specify known properties, and 'users' does not exist in type I'd add |
For includes, you should just manually add it via a cast. The old typings didn't really handle it either, and it'd be complicated to near-impossible to make this work automatically. Here's what we do in our code interface ProjectAttributesWithUser extends ProjectAttributes {
User: UserAttributes;
}
const project = await Project.findAll({ ...someOptions, include: [User] }) as ProjectAttributesWithUser[]; |
Pull Request check-list
Please make sure to review and check all of these items:
npm run test
ornpm run test-DIALECT
pass with this change (including linting)? **Also runnpx tsc --project types/test/
andnpx tsc --project types/
Description of change
This code allows users to have typechecking for model attributes in all relevant functions in the
Model
class, as well as some functions outside of theModel
class. This was inspired by the Sequelize v4 DefinitelyTyped typings, but is made to be almost fully backwards compatible for users of the current typings.Specific things I tried to ensure:
sequelize.define
as well asnew Model
formats of building modelsHere are the things I changed:
_attributes
and_creationAttributes
) to the Model class so that the static functions can be typed correctlymodel.d.ts
TAttributes
automaticallymodel.d.ts
model.d.ts
to use the new typings, and made others just fallback to just usingany
for the generic argumentstypes/tests
Testing
In addition to adding a few tests and modifying the tests for
User.ts
anddefine.ts
to use the new typings, I also have this running on a codebase with ~20k LOC for https://wanderlog.com - everything typechecks correctly, and it's an improvement from our v4 typings.Questions for reviewers
Option
interfaces likeFindOptions
now has a required generic parameter. Should I make this parameter also optional to make it fully backwards-compatible?doFancyFind(option: FindOptions)
won't need to change it todoFancyFind(option: FindOptions<any>)
with this changeOptional
type in the typings inindex.d.ts
? It's very useful for constructing the optional types and can help