Using Typescript interface as a variable - javascript

I am working on a Node Js (TypeScript) architecture and for some reason, I want to bind my interface to a specific object. I am making a general class that is extended by other subclasses and it will have a very general code. So my code looks like
interface User {
name: string;
}
interface Profile {
title: string;
}
class Parent {
name: string;
interface: Interface; // Help required here, getting error can't use type as a variable
constructor( name, interface ) {
// Load schema and store here
this.name = name
this.interface = interface
}
// Though this is not correct I hope you get the idea of what I am trying to do
get (): this.interface {
// fetch the data and return
return data
}
set (data: this.interface): void {
// adding new data
}
}
class UserSchema extends Parent {
// Class with custom functions for UserSchema
}
class ProfileSchema extends Parent {
// Class with custom functions for ProfileSchema
}
// Config file that saves the configs for different modules
const moduleConfig = [
{
name: "User Module",
class: UserSchema,
interface: User
},
{
name: "Profile Module",
class: ProfileSchema,
interface: Profile
},
]
const allModules = {}
// Loading the modules
moduleConfig.map(config => {
allModules[config.name] = new config.class(
config.name,
config.interface
)
})
export allModules;
I need suggestions on how should I bind my interfaces with their respective configs. Till now I have had no luck with that.
PS: All this code is separated into their respective files.

This is the use case for generics. You can even see them as "variable for types".
Instead of having an interface property in your Parent class, the latter would have a generic type:
class Parent<T> { // T is the generic type
name: string;
// interface: Interface; // generic is already provided at class level
constructor( name ) {
// Load schema and store here
this.name = name
}
get (): T {
// fetch the data and return
return data
}
set (data: T): void {
// adding new data
}
}
// Here you specify the concrete generic type
class UserSchema extends Parent<User> {
// Class with custom functions for UserSchema
}
class ProfileSchema extends Parent<Profile> {
// Class with custom functions for ProfileSchema
}

Related

How to create factories with existing relationship on model in MIKRO-ORM

Hi I am trying to figure out how to create factory and define relationship between models.
For example I have UserFactory with User entity and this entity has connection to userType table. In factory I have not access to EntityManager so I couldnĀ“t find any existing.
export class UserFactory extends Factory<User> {
model = User
definition(faker: Faker): Partial<User> {
const user = {
firstName: faker.name.firstName(),
lastName: faker.name.lastName(),
...
userType: // Here I need do something like this:
// EntityManager.findOne(UserType, {id: 1}}
// But EntityManager is private in Factory class
}
return user
}
}
Itried also something like this but this return me an error:
ValidationError: Value for User.type is required, 'undefined' found
DatabaseSeeder
export class DatabaseSeeder extends Seeder {
async run(em: EntityManager): Promise<void> {
const users: User[] = new UserFactory(em).each(async user => {
const userType : UserType| null = await em.findOne(UserType, 1)
console.log(tenant)
const userType = await em.findOne(UserType, 1)
if (userType !== null) {
user.type = userType
} else {
user.type = em.create(UserType, {
type: 'test'
})
}
}).make(10)
}
}
What is the proper way to achieve this please?
You can use the shared seeder context as describer in the docs:
https://mikro-orm.io/docs/seeding#shared-context
export class AuthorSeeder extends Seeder {
async run(em: EntityManager, context: Dictionary): Promise<void> {
// save the entity to the context
context.author = em.create(Author, {
name: '...',
email: '...',
});
}
}
export class BookSeeder extends Seeder {
async run(em: EntityManager, context: Dictionary): Promise<void> {
em.create(Book, {
title: '...',
author: context.author, // use the entity from context
});
}
}
I guess this shared context should be also available in the seeder factories, but you can always handle this yourself, as both the seeder and factory is your implementation, so you can pass any additional options in there. Its you who initializes the factory so I dont think there is a better way than doing it in your code.
I would suggest not to flush and findOne things in your seeder, you should aim for a single flush and use the shared context instead for entity look up.

Mongoose loadClass issue with TypeScript

I'm taking advantage of mongoose class schemas.
And using TypeScript for my Node project.
I've followed Mongoose the Typescript way...? to make sure my Model is aware of the schema I've defined, So I've auto-completion etc..
However it becomes more tricky with schema class.
As written in their docs:
The loadClass() function lets you pull in methods, statics, and
virtuals from an ES6 class. A class method maps to a schema method, a
static method maps to a schema static, and getters/setters map to
virtuals.
So my code looks something like:
interface IUser extends mongoose.Document {
firstName: string,
lastName: string
};
const userSchema = new mongoose.Schema({
firstName: {type:String, required: true},
lastName: {type:String, required: true},
});
class UserClass{
static allUsersStartingWithLetter(letter: string){
return this.find({...});
}
fullName(this: IUser){
return `${this.firstName} ${this.lastName}`
}
}
userSchema.loadClass(UserClass);
const User = mongoose.model<IUser>('User', userSchema);
export default User;
My goal is that TypeScript will understand that:
User has a method allUsersStartingWithLetter
User instance has a method fullName
In the current configuration it does not.
I was not able to accomplish it myself.
Have you considered adding extends mongoose.Model to the UserClass?
class UserClass extends mongoose.Model<IUser> {
static allUsersStartingWithLetter(letter: string){
return this.find({...});
}
fullName(this: IUser){
return `${this.firstName} ${this.lastName}`
}
}
Do you really need to use classes? You could accomplish this using interfaces without using classes to do it. Here's an example:
/* eslint-disable func-names */
import mongoose from 'mongoose';
export interface Foo {
id?: string;
name: string;
createdAt?: Date;
updatedAt?: Date;
}
export type FooDocument = mongoose.Document & Foo;
const fooSchema = new mongoose.Schema(
{
name: { type: String, required: true },
},
{ timestamps: true }
);
fooSchema.methods.bar = function (): void {
const foo = this as FooDocument;
foo.name = 'bar';
};
const FooModel = mongoose.model<FooDocument>('foos', fooSchema);
export default FooModel;
This way you can use the Foo interface for methods with the inversion depedency. Them in your repository will return Foo instead of FooDocument...
Extra: If you use lean() in your database requests you return exactly the Foo interface. More information for lean here
Your UserClass needs to extend Model from mongoose. You seem to be missing a bit of required code to make this work for you.
From the link you shared as a reference, here's a guide that should solve your issue with complete code example.
https://stackoverflow.com/a/58107396/1919397

How to define custom query helper in mongoose model with typescript?

I want to define custom query helper using query helper api .
Here the example:
// models/article.ts
import { Document, Schema, Model, model } from 'mongoose';
interface IArticle extends Document {
name: string;
}
interface IArticleModel extends Model<IArticle> {
someStaticMethod(): Promise<any>;
}
const ArticleSchema = new Schema( { name: String } )
ArticleSchema.query.byName = function(name) {
return this.find({ name })
}
export default model<IArticle, IArticleModel>('Article', ArticleSchema);
// routes/article.ts
import ArticleModel from '../models/article.ts'
router.get('/articles, (req, res) => {
ArticleModel.find().byName('example')
})
Typescript complains about byName method when I chain it with defaults.
I can put it in IArticleModel interface but in that case I could only call it from model.
Where should I put the definition of this method to use it in chainable way?
I've drafted a new version of #types/mongoose that supports query helpers. See this answer for ways to install a modified #types package. With my version, you should be able to write the following in models/article.ts:
import { Document, Schema, Model, model, DocumentQuery } from 'mongoose';
interface IArticle extends Document {
name: string;
}
interface IArticleModel extends Model<IArticle, typeof articleQueryHelpers> {
someStaticMethod(): Promise<any>;
}
const ArticleSchema = new Schema( { name: String } )
let articleQueryHelpers = {
byName(this: DocumentQuery<any, IArticle>, name: string) {
return this.find({ name });
}
};
ArticleSchema.query = articleQueryHelpers;
export default model<IArticle, IArticleModel>('Article', ArticleSchema);
and then routes/article.ts will work. If this works for you, then I will submit a pull request to the original package on DefinitelyTyped.

Let classes inherit from type aliases

I try to create a user class and want to be able to inherit from a type alias:
type PlainUser = { email: string }
class User extends PlainUser {
constructor (initialValues: PlainUser) {
this.email = initialValues.email
}
update () { ... }
}
This doesn't work of course, but I would like to have the following semantics without having to duplicate email (and all the other fields that I don't show to keep it brief):
type PlainUser = { email: string }
class User {
email: string
constructor (initialValues: PlainUser) {
this.email = initialValues.email
}
update () { ... }
}
Is this possible with flow?
Not that I know of, but you can at least use implements to enforce that the User class implements the PlainUser interface (yes, you have to change it to be an interface).
interface PlainUser {
email: string;
}
class Foo implements PlainUser {
}
(tryflow)
The code above yields the following error with Flow v0.41, since Foo does not specify an email property:
7: class Foo implements PlainUser {
^ property `email` of PlainUser. Property not found in
7: class Foo implements PlainUser {
^ Foo
Of course, this isn't exactly what you've asked for. But at least you are getting automatic checking that User implements PlainUser, rather than nothing at all.
You can only extend from classes, and your type alias is an interface, so you have to use implement here. TypeScript Salsa allows doing the following since this suggestion was implemented:
type PlainUser = { email: string };
class User implements PlainUser {
constructor (initialValues: PlainUser) {
this.email = initialValues.email;
}
}
If you do not use salsa, you have to explicitly declare the inherited properties:
type PlainUser = { email: string };
class User implements PlainUser {
public email: string;
constructor (initialValues: PlainUser) {
this.email = initialValues.email;
}
}
Playground
I'll admit this was a head scratcher initially, but something like what you want to do is very possible. It does require rethinking the approach a bit.
First, you need to start with the class instead of the object literal. Intuitively this makes sense, as that's also the way javascript works.
class User {
email: string;
}
Next you want to use flow's $Shape transformation. This will cast your type to the enumerable properties of the class.
type PlainUser = $Shape<User>;
const Bob: PlainUser = { email: "bob#bob.com" }
or
const BobProperties: PlainUser = { ...new PlainUserClass("bob#bob.com") }
Finally, extend the User class as normal.
class AdminUser extends User {
admin: true;
}
example

Use imports inside interface definition

I'm having a strange problem with typescript interfaces. Because I'm using mongoose models I need to define one, but for some reason it's not recognising things that I have explicitly imported. This part works fine:
export interface ITrip extends mongoose.Document {
//
}
export var TripSchema = new mongoose.Schema({
//
});
export var Trip = mongoose.model<ITrip>('Trip', TripSchema);
Now, I'm defining another interface, that has an array of Trip. I need this for subdocuments.
import {Trip, ITrip} from '../trips/trip.model';
export interface IFeed extends mongoose.Document {
lastSnapshot: {
trips: [Trip]
}
}
The TS compiler gives this error: feed.ts(12,13): error TS2304: Cannot find name 'Trip'. (referring to trips: [Trip]). It doesn't say that the import failed or anything. I can even use trip inside the same file to create new objects var a = new Trip({}); without problem. Inside the interface it breaks.
Trip isn't a type, it's a variable, so you can do this:
let t = Trip;
let t2 = new Trip({});
But you can't do this:
let t: Trip;
You should change it to typeof Trip:
export interface IFeed extends mongoose.Document {
lastSnapshot: {
trips: [typeof Trip]
}
}
Also, if you want IFeed.lastSnapshot.trips to be an array, then it should be:
trips: typeof Trip[]
What you declared is a tuple of one item.
Edit
With an object the assignment is always the same (both js and ts):
let o = {
key: "value"
}
But when declaring types in typescript then you're not dealing with values:
interface A {
key: string;
}
let o: A = {
key: "value"
}
In the mongoose documentation they are using only javascript so all of their examples don't include the type declarations.

Categories