Skip to content
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): add InferAttributes utility type #13909

Merged
merged 29 commits into from Jan 21, 2022
Merged

feat(types): add InferAttributes utility type #13909

merged 29 commits into from Jan 21, 2022

Conversation

ephys
Copy link
Member

@ephys ephys commented Jan 5, 2022

Pull Request Checklist

  • Have you added new tests to prevent regressions?
  • Does npm run test or npm run test-DIALECT pass with this change (including linting)?
  • Is a documentation update included (if this change modifies existing APIs, or introduces new ones)?
  • Did you update the typescript typings accordingly (if applicable)?
  • Does the description below contain a link to an existing issue (Closes #[issue]) or a description of the issue you are solving?
  • Did you follow the commit message conventions explained in CONTRIBUTING.md?

Description Of Change

This PR introduces a few new utility types:

  • InferAttributes<M, opts>: Used to drastically reduce the boilerplate needed to define Model Attributes in the class-based approach.
  • InferCreationAttributes<M, opts>: Like InferAttributes but fields tagged with CreationOptional<x> are optional.
  • CreationOptional<x>: A branded type used to mark an attribute as optional in Creation Attributes.
  • NonAttribute<x>: A branded type used to omit a property from the attribute list.
  • Attributes<M>: An alias for M['_attributes'] for public use. Also forward compatible with planned v7 changes.
  • CreationAttributes<M>: Same as Attributes but types that accept undefined are also optional.

To reduce risk of collision between Model and its subclasses, Model#_attributes and Model#_creationAttributes have been deprecated (I'd like to replace them with symbols in v7). Use Attributes<Model> and CreationAttributes<Model> instead.

In v7, we should replace these two with non-exported unique symbols.

Copy link
Member

@WikiRik WikiRik left a comment

Have some comments, but not on the content itself. I do not feel experienced enough on this to really comment on that

docs/manual/other-topics/typescript.md Outdated Show resolved Hide resolved
docs/manual/other-topics/typescript.md Outdated Show resolved Hide resolved
docs/manual/other-topics/typescript.md Outdated Show resolved Hide resolved
docs/manual/other-topics/typescript.md Outdated Show resolved Hide resolved
docs/manual/other-topics/typescript.md Outdated Show resolved Hide resolved
types/test/typescriptDocs/ModelInitWithAttributesOf.ts Outdated Show resolved Hide resolved
types/test/typescriptDocs/ModelInitWithAttributesOf.ts Outdated Show resolved Hide resolved
types/test/typescriptDocs/ModelInitWithAttributesOf.ts Outdated Show resolved Hide resolved
types/test/typescriptDocs/ModelInitWithAttributesOf.ts Outdated Show resolved Hide resolved
types/test/typescriptDocs/ModelInitWithAttributesOf.ts Outdated Show resolved Hide resolved
@WikiRik
Copy link
Member

@WikiRik WikiRik commented Jan 5, 2022

Woops, did not notice it was in draft still. So some of my comments might be obvious to you

@ephys
Copy link
Member Author

@ephys ephys commented Jan 5, 2022

No no, thank you for the review :) I did parts of it in a hurry to go eat and didn't notice most of the things you mentioned

@ephys ephys requested a review from WikiRik Jan 5, 2022
@ephys ephys marked this pull request as ready for review Jan 5, 2022
@ephys ephys added the type: typescript label Jan 5, 2022
@ephys ephys self-assigned this Jan 5, 2022
@ephys
Copy link
Member Author

@ephys ephys commented Jan 5, 2022

The mapped types I'm using in this PR require TS >= 4.1. We'd need to drop support for TS <= 4.1 to merge this.

I'm on board with it. It's more than a year old now. The question is: do we want to consider that a semver major or minor?

@WikiRik
Copy link
Member

@WikiRik WikiRik commented Jan 5, 2022

The mapped types I'm using in this PR require TS >= 4.1. We'd need to drop support for TS <= 4.1 to merge this.

I'm on board with it. It's more than a year old now. The question is: do we want to consider that a semver major or minor?

We had this discussion in another PR as well and I had difficulty with finding a good support plan for TypeScript versions. One that I did find was DefinitelyTyped which tests on TypeScript versions that are less than 2 years old. But if you find other examples I would be fine with shortening the support window.

Since TypeScript releases come out approximately every three months and the minority of users use TypeScript, I would make this a semver minor change.

@WikiRik WikiRik requested review from AllAwesome497, fzn0x and Keimeno Jan 5, 2022
@ephys
Copy link
Member Author

@ephys ephys commented Jan 5, 2022

We can use typesVersions to expose different typing files based on TS version. I can try to do something with it.
Alternatively we drop support for versions < 4.1 8 months early (4.0 having been released in august 2020)

@ephys
Copy link
Member Author

@ephys ephys commented Jan 5, 2022

Before we merge this, I'm also thinkings of ways to solve the TCreationAttributes part:

I have a solution that uses a type as branding to mark attributes as optional in TCreationAttributes:

image

It's a bit hacky but so far it works.

If we implement this as well as #14302, in v7 we should be able to simplify the Model definition to simply:

class User extends Model<User> {}

Model should be able to extract all the necessary information from the User class directly.

It's implemented like this:
declare const CreationAttributeBrand: unique symbol;

type CreationAttribute = { [CreationAttributeBrand]: true };

export type CreationOptional<T> = T & CreationAttribute;

export type CreationAttributesOf<M extends Model, Excluded extends string = ''> = {
  [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<infer Val> ? (Val | undefined)
    : M[Key]
};

and the definition of create in model.d.ts has been changed to this:

type UndefinedPropertiesOf<T> = {
  [P in keyof T]-?: undefined extends T[P] ? P : never
}[keyof T];

type OptionalUndefined<T extends object> = Optional<T, UndefinedPropertiesOf<T>>;

class Model {

  public static create<
    M extends Model,
    O extends CreateOptions<M['_attributes']> = CreateOptions<M['_attributes']>
  >(
    this: ModelStatic<M>,
    values?: OptionalUndefined<M['_creationAttributes']>,
    options?: O
  ): Promise<O extends { returning: false } | { ignoreDuplicates: true } ? void : M>;
}

Copy link
Member

@AllAwesome497 AllAwesome497 left a comment

Not an issue with this PR, but I think these associations are a bit too complex - its very easy to forget some.

docs/manual/other-topics/typescript.md Outdated Show resolved Hide resolved
docs/manual/other-topics/typescript.md Outdated Show resolved Hide resolved
@ephys
Copy link
Member Author

@ephys ephys commented Jan 7, 2022

I've added a solution for creation attributes, but I went with a different direction than the branded type as it was causing issues.

Instead I went with a second option for AttributesOf: optional. This way it can be used for TCreatrionAttributes too:

class User extends Model<
  AttributesOf<User, { omit: 'projects' }>,
  AttributesOf<User, { omit: 'projects', optional: 'id' }>
> {
  declare id: number;
  declare name: string;
  declare projects?: Project[];
}

Of course this double definition is due to backward compatibility requirements in v6.

In v7, I'll make it look like this instead:

class User extends Model<User, { omit: 'projects', creationOptional: 'id' }> {
  declare id: number;
  declare name: string;
  declare projects?: Project[];
}

Unless we find a solution for branded types

@ephys
Copy link
Member Author

@ephys ephys commented Jan 7, 2022

Oh no, I found a way to make a branded type that works.

I'll revert some things.

image

Updated way of doing this is therefore back to:

class User extends Model<AttributesOf<User, { omit: 'projects' }>, CreationAttributesOf<User, { omit: 'projects' }>> {
  declare id: CreationOptional<number>;
  declare name: string;
  declare projects?: Project[];
}

I've also added NonAttribute as an alternative to omit:

class User extends Model<AttributesOf<User>, CreationAttributesOf<User>> {
  declare id: CreationOptional<number>;
  declare name: string;
  declare projects?: NonAttribute<Project[]>;
}

@ephys
Copy link
Member Author

@ephys ephys commented Jan 16, 2022

Well that's annoying, esdoc* fails on numeric separators & optional catch bindings. I'm not sure why it didn't complain about it during the eslint PR. I'll revert these changes and we'll do them again once #13914 is finalized

@WikiRik
Copy link
Member

@WikiRik WikiRik commented Jan 16, 2022

esbuild or esdoc?

@ephys
Copy link
Member Author

@ephys ephys commented Jan 16, 2022

esdoc* :)

@ephys
Copy link
Member Author

@ephys ephys commented Jan 16, 2022

I think I figured what the issue was. It's not related to the new linting rules. It just did not like seeing HTML comments in a markdown file.

@ephys
Copy link
Member Author

@ephys ephys commented Jan 16, 2022

I think this is good to go now, just need a review 🙂

Copy link
Member

@WikiRik WikiRik left a comment

Looks good. Few small remarks. I want to wait for the approval of @AllAwesome497 before we merge

docs/manual/other-topics/typescript.md Outdated Show resolved Hide resolved
types/lib/associations/belongs-to-many.d.ts Outdated Show resolved Hide resolved
types/lib/associations/has-many.d.ts Outdated Show resolved Hide resolved
types/lib/hooks.d.ts Show resolved Hide resolved
types/lib/sequelize.d.ts Outdated Show resolved Hide resolved
AllAwesome497
AllAwesome497 previously approved these changes Jan 21, 2022
Copy link
Member

@AllAwesome497 AllAwesome497 left a comment

looks fine to me

*/
_attributes: TModelAttributes;
_attributes: TModelAttributes; // TODO [>6]: make this a non-exported symbol (same as the one in model.d.ts)
Copy link
Member

@AllAwesome497 AllAwesome497 Jan 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe make this private? (same with the other places where this comment is)

Copy link
Member Author

@ephys ephys Jan 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't yet, it would break user code, but I can in v7

Copy link
Member

@WikiRik WikiRik Jan 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add that to the TODO as well?

Copy link
Member Author

@ephys ephys Jan 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that I think of it, if it's private, Attribute<M> will not be able to access it. It's going to have to be a public internal Symbol I think

@ephys
Copy link
Member Author

@ephys ephys commented Jan 21, 2022

@WikiRik small documentation update: I've added the v6 expected release version, fixed the documentation using the old utility types, and documented Attributes<Model>, CreationAttributes<Model>, and ModelStatic<Model>

If there are no issues with these changes, we can merge

Copy link
Member

@WikiRik WikiRik left a comment

New changes look good so we can merge (possibly after the TODOs have been updated, but I can always approve again)

@ephys ephys merged commit 7eff04a into main Jan 21, 2022
42 checks passed
@ephys ephys deleted the feature/attributesof branch Jan 21, 2022
@sdepold sdepold moved this from To be released to Releasing in Sequelize v6 release log Jan 22, 2022
@sdepold sdepold moved this from Releasing to To be released in Sequelize v6 release log Jan 22, 2022
@sdepold sdepold moved this from To be released to Releasing in Sequelize v6 release log Jan 22, 2022
sdepold pushed a commit that referenced this issue Jan 22, 2022
Co-authored-by: Rik Smale <13023439+WikiRik@users.noreply.github.com>
@sdepold sdepold moved this from Releasing to Released in Sequelize v6 release log Jan 23, 2022
@github-actions
Copy link
Contributor

@github-actions github-actions bot commented Feb 7, 2022

🎉 This PR is included in version 7.0.0-alpha.6 🎉

The release is available on:

Your semantic-release bot 📦🚀

maramizo pushed a commit to maramizo/sequelize that referenced this issue Jun 2, 2022
Co-authored-by: Rik Smale <13023439+WikiRik@users.noreply.github.com>
aliatsis pushed a commit to creditiq/sequelize that referenced this issue Jun 2, 2022
Co-authored-by: Rik Smale <13023439+WikiRik@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
released on @v7 type: typescript
Projects
Development

Successfully merging this pull request may close these issues.

None yet

3 participants