Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

Typing Sequelize association mixins using TypeScript

Posted on Sep 26 When using the Sequelize ORM package, it is highly likely you need to create associations between models at some point. Using the example models:You can then associate them using one of the Association methods on the model:Now Foo is associated to Bar.As per Sequelize's documentation:When an association is defined between two models, the instances of those models gain special methods to interact with their associated counterparts.So, when calling Model.belongsTo (or any other association methods), some methods are added to the instances of said Model. Now, you know these methods exist, but TypeScript does not. So how would we type these methods?The @types/sequelize package used to get types for Sequelize is kind to us by providing pre-declared generic types for each of the mixin methods, using these, you could type the methods like this:Now this is easy and straightforward, but this method has a handful of issues:So this method is clearly not ideal, it bloats the code, makes it less maintainable by needing to remember to change many lines altogether... Can we do better?We have seen that typing each mixin method individually in the model definition is not ideal, so how can we refactor the typings to type every mixin at once for an association?Well first, we need to reason about an association's mixins, so let's extract them to a type:Ok, we have the mixins all together now, can we refactor it further? Maybe make a generic type for belongsTo associations? There are a few types we could use for configuration like the model and the type of the model's primary key!...But each of the property names are derived from the association name we give Sequelize (derived from the other model's name when unspecified). Thankfully, this automatic naming follows a few rules we could use too!We know we can make the property types configurable easily using generics, but can we make the property names configurable too?the following sections tagged as "experiment" are steps I took when trying to solve the issue, which led to the solution, if you only care about the conclusion, feel free to skip themLet's experiment a bit and make a type which only has one configurable property. For that, we can use template literal types for basic concatenation of string types, something like this maybe?error TS1170: A computed property name in a type literal must refer to an expression whose type is a literal type or a 'unique symbol' type.Ah... this syntax expects a value between the brackets, which may only be a literal or symbol. We need a syntax that would accept a type and create a property from it, also what would happen if Name was a union of string literal types? Maybe it should create multiple Properties, `set${Capitalize}`, would already be a string union according to TypeScript's rules on template literal types...I get it! we need to use a mapped type!No error, if we test it with:That works! Now let's how do we combine multiple configurable propertiesNow let's try making multiple configurable properties in a single type, as we know we need at least 3 to reach our goal. Let's try putting the mapping syntax multiple times in the same object type!error TS7061: A mapped type may not declare properties or methods.Huh? Well turns out TypeScript thinks the second mapping is trying to declare a property using brackets, like we tried earlier, and we cannot declare properties in a mapped type. So we could do two, but how do we combine them?Considering mapped type A and B, our new type needs to be both A and B, which is a type intersection!Again, we get no error, and it just works as expected!But that pile of syntax, while it shows that what we want is possible, is a bit annoying to write. Can we refactor that into something more useable?Let's write a simple-ish type that will allow us to write:The magic lies in the generic PostfixProperties type, so how do we write it anyway? We use mapped types once again, and their ability to rename the output object type's properties:Let's take a minute to dissect this type. First, it is a mapped type over the keys of the PropTypes type parameter, however, we rename the properties to include the original name and append the provided Postfix. When concatenating, we need to exclude symbol properties of our original object since they cannot appear in a template literal type. Finally, the type of the renamed property is set to its original type.Let's get back to the original task at hand: typing Sequelize mixin methods. For that we needed a generic type to define the mixin methods with appropriate names and types. We now have all the pieces we need, so let's just do that:And it works, we have our generic type for a belongsTo association's mixins! hurray!The definition of the hasOne other association types are left as an exercise for the reader.For the many-to-many relation types, not a lot changes, but first, let's define a Prettify type:This types does not change the type of T so long as it is an object type, but it helps typescript tooling show intersections of object types as a single object type. Speaking of object intersection, we will use it to come up with a hasMany association mixin type:The mixin type for belongsToMany associations is left as an exercise, because it is basically the same.Now we can generate mixin interface types, but how do we use it with our models?Our models are defined as JS classes extending from the base Model class from sequelize, in TypeScript we can also add an implements clause for additional interfaces. "But, our types are not defined as interfaces, we cannot implement them?" you may ask, however in TypeScript, you can actually implement any type that is "interface-like", that is, types that represent things describable by interfaces, so our object type alias (or intersection of object types) is useable as an interface.So that's it, right? Wrong. Because implements checks the instance type of the class (according to its body and parent hierarchy), and finds the properties we said existed do not. So what do we do? Do we give in and actually write all the bloat we didn't want, with the only benefit of having it type-checked? We have tools to do that, oh and we have to make sure each and every one of them is a declare-ed property because we do not control the implementation...Well, while it would still be better, we don't have to, thanks to properties of the class syntax and interfaces in TypeScript.When defining a class in TypeScript, you actually define two things at once: the run-time class value, and the class instance type. So when class Foo {} is defined, we have both a local Foo value, which is the class constructor, and a Foo type for the class instances. The class instance type becomes interesting to us since it is not defined like a type alias, but like an interface.Interfaces in TypeScript have the interesting behavior called "interface merging", where if two interfaces of the same name are defined in the same scope, the properties of both declarations merge and leaves only one type with all the properties. We can also have interfaces extend others, and multiple ones at a time, to inherit their properties. With this we can effectively add properties to any interface we want.Combining these, we can emulate a sort of "declare implements" functionality like so:With that, all our instances will have the correctly typed mixin methods, and we didn't have to bloat our class definition with declarations to add them.The code is also available as a TypeScript playgroundTemplates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well Confirm For further actions, you may consider blocking this person and/or reporting abuse MFONIDO MARK - Sep 16 Ayomikun Sholademi - Sep 10 Lucas Carmichael - Sep 20 raman04-byte - Sep 19 Once suspended, thetos_dastil will not be able to comment or publish posts until their suspension is removed. Once unsuspended, thetos_dastil will be able to comment and publish posts again. Once unpublished, all posts by thetos_dastil will become hidden and only accessible to themselves. If thetos_dastil is not suspended, they can still re-publish their posts from their dashboard. Note: Once unpublished, this post will become invisible to the public and only accessible to Thetos. They can still re-publish the post if they are not suspended. Thanks for keeping DEV Community safe. Here is what you can do to flag thetos_dastil: thetos_dastil consistently posts content that violates DEV Community's code of conduct because it is harassing, offensive or spammy. Unflagging thetos_dastil will restore default visibility to their posts. DEV Community — A constructive and inclusive social network for software developers. With you every step of your journey. Built on Forem — the open source software that powers DEV and other inclusive communities.Made with love and Ruby on Rails. DEV Community © 2016 - 2023. We're a place where coders share, stay up-to-date and grow their careers.



This post first appeared on VedVyas Articles, please read the originial post: here

Share the post

Typing Sequelize association mixins using TypeScript

×

Subscribe to Vedvyas Articles

Get updates delivered right to your inbox!

Thank you for your subscription

×