Sometimes we may need to have a type reference itself in its attributes, or have two types that reference eachother in separate files, it can be a complication because it's not possible to do this using the type definitions like we did before. For cases like this we can use a feature called "dynamic types".
When using dynamic types you can pass a string instead of the type itself in the type definition, this string will contain the type identifier, and then pass an object as the second parameter of the attributes function with a key dynamics where the concrete type for each identifier is declared:
The type's identifier has to be same everywhere it's used, and can be defined in two ways:
Inferred identifier
The identifier can be inferred based on the class that is wrapped by the attributes function. In backend scenarios this will be the most common case:
const User = attributes(
{
name: String,
bestFriend: 'User', // [A] type with inferred identifier
},
{
dynamics: {
User: () => User, // [B] inferred identifier concrete type
},
}
)(
class User {
// ⬑-- the name of this class is the identifier
// so if we change this name to UserEntity, we'll have to change
// both [A] and [B] to use the string 'UserEntity' instead of 'User'
}
);
Custom identifier
If for some reason you can't rely on the class name, be it because you're using a compiler that strips class names or creates a dynamic one, you can explicitly set an indentifier.
To do that, in the second argument of the attributes function (e.g. the options) you should add a identifier key and set it to be the string with the type's identifier and then use that custom value everywhere this type is dynamically needed:
const User = attributes(
{
name: String,
bestFriend: 'UserEntity', // << type with custom identifier
},
{
identifier: 'UserEntity', // << custom identifier
dynamics: {
// ⬐--- custom identifier concrete type
UserEntity: () => User,
},
}
)(class User {});
Concrete type definition inside dynamics
For the cases where the dynamic type is in a different file, it's important that the actual needs type to be resolved inside the function with the identifier. Let's break it down in two cases:
With CommonJS modules
When using CommonJS modules you have two possibilities:
Putting the require call directly inside the concrete type resolution function:
const Book = attributes(
{
name: String,
owner: 'User',
nextBook: 'BookStructure',
},
{
identifier: 'BookStructure',
dynamics: {
User: () => require('./User'), // << like this
BookStructure: () => Book,
},
}
)(class Book {});
module.exports = Book;
Exporting an object containing your other structure (instead of exporting the structure itself) and only access it inside the concrete type resolution function:
If you're using import instead of require (thus having to import the dynamic value in the top-level of the file), go to the section of this page.
When using ES Modules you have a single possibility, which is importing the other structure from the top-level of your file and then returning it from the concrete type resolution function. It's important to note that this only works with ES Modules, if you're using CommonJS, check the section.