Let's say I have two entities: Vehicle and Tariff. They are related as 1-to-many (vehicle can has many tariffs).
This is my basic types definition of objects, I want to work with:
interface Tariff {
id: string; // <-- ID of entity
createdAt: Date; // <-- some meta data fields for sorting and etc.
days: number; // <-- userful info
price: number; // <-
}
interface Vehicle {
id: string; // <-- ID of entity
createdAt: Date; // <-- some meta data fields for sorting and etc.
model: string; // <-- userful info
year?: string; // <-
seatsCount?: number; // <-
tariffs: Tariff[]; // <-- array of tariffs
}
Looks great. No we need to create database model. First let's define mongoose schema:
import mongoose from "mongoose";
const vehicleSchema = new mongoose.Schema({
createdAt: {type: Date, required: true, default: () => new Date()},
model: {type: String, required: true},
year: String,
seatsCount: Number,
});
Awesome. I decided no to put tariffs inside vehicle documents and they will be stored separately.
Next we need to create mongoose model. But we need to provide generic type extending mongoose.Document
to mongoose.model
. And this is where code duplication begins:
// First we need to define TypeScript schema equalent of mongoose schema:
interface VehicleSchema {
createdAt: Date;
model: string;
year?: string;
seatsCount?: number;
}
// Then we need to defined a new type of out vehicle models:
type VehicleModel = VehicleSchema && mongoose.Document;
// And finaly we can create model:
const VehicleModel = mongoose.model<VehicleModel>("Car", carSchema);
// Look, here is a type named "VehicleModel" and a constant named "VehicleModel"
Now we can write functions for CRUD operations:
namespace vehicle {
export const create = async (form: VehicleCreateForm): Promise<Vehicle> => {
const fields = toDb(form);
// assume form is prevalidated
let dbVehicle = new VehicleModel(form);
dbVehicle = await model.save();
// tariff - is the separate module, like this one,
// createAllFor methods retuns Tariff[]
const tariffs = await tariff.createAllFor(dbVehicle.id, form.tariffs);
return fromDb(dbVehicle, tariffs);
}
//...
// export const update = ...
// export const list = ...
// export const get = ...
// etc..
}
Here we intoduce one extra type: VehicleCreateForm
, it describes all fields needed to create vehicle:
interface VehicleCreateForm {
model: string;
year?: string;
seatsCount?: number;
tariffs: TariffCreateFields[]; // <-- here is another one special type
}
interface TariffCreateFields {
days: number;
price: number;
}
Also we need to define two functions: toDb
to prevent some fields, f.e. tariffs
be passed to model. And fromDb
"translates" model to Vehicle
entity, removes or converts some fields:
const u = <T>(value: T | undefined | null): (T | undefined) => value === null ? undefined : value;
const toDb = (f: VehicleCreateForm): VehicleCreateFields => ({
model: f.model,
year: f.year,
seatsCount: f.seatsCount,
});
const fromDb = (m: VehicleModel, tariffs: Tariff[]): Vehicle => ({
id: m.id,
createdAt: m.createdAt,
model: m.model,
year: u(m.year),
tariffs,
});
And, yaaa, we need one more extra type: VehicleCreateFields
. Fields we are passing to model constructor.
interface VehicleCreateFields {
model: string;
year?: string;
seatsCount?: number;
}
Seems like here is done.
Also we need to define tariff
namespace similar to vehicle
. All types and will be duplicated too: TariffSchema
, TariffModel
, TariffCreateForm
, TariffCreateDocument
.
And this will happen for every new entity. In current case I can avoid creating VehicleUpdateFields
type and use VehicleCreateFields
for creating and updating. There could be also VehicleUpdateDocument
and etc.
How can I reduce the amount of code duplications. How you deal with typescript/mongoose?
Ofcouse I can extract common fields to "common chunck" interfaces, but I event don't know how to name them. I tried to use interface inherence, end up with messy hell of small types. Like:
interface Entity {
id: string;
createdAt: Date;
}
interface VehicleOwnFields {
model: string;
year?: string;
seatsCount?: number;
}
interface VehicleOwnFields {
model: string;
year?: string;
seatsCount?: number;
}
interface VehicleExternalFields {
tariffs: Tariff[];
}
type Vehicle = Entity && VehicleOwnFields && VehicleExternalFields;
// and so on...
I think this is not the best solution.
Also there can be cases when fields has different types in Entity and in Schema. F.e. I can decide to serialize Tariff[]
to JSON and store as string field in VehicleModel
.
I think this is general problem for typescript and ORMs.
If you know good articles where this problem is described, please give me link.
Top comments (12)
You could do
Leveraging generics is probably the way to go.
Ok, this is good one. Thank you.
How can I optimize other fields?
I'm on mobile at the moment. I'll post some other suggestions in a bit when I'm back at my laptop. On general though, union types and generics really help with these kinds of situations to make types more maintainable.
From there you could even do a Mongoose document generic type.
You could refine this more if you wanted to make a Vehicle type which just wraps the
Entity<T>
.You can see it in action in the enhanced TypeScript playground.
Also, this might interest you.
An Enhanced TypeScript Playground
Nick Taylor
Ok, I will play with it. Thank you.
You can even go a bit further.
See the updated playground example
You could always use something like TypeORM. I haven't try it myself, as the last time I needed an ODM I used camo but I think it has been deprecated now.
I recently found typegoose, which seems promising and seems to attempt to solve this problem. Disclaimer: I haven't used it.
I will say, I'm about a year into a codebase with typescript and mongoose, and I wish I would've just started with TypeORM, and at some point I may switch. Mongoose seems like a project that's either already dead or on life-support.
Simple: Use Python!
...
Yes, I am absolutely, undeniably kidding. I've heard that joke in my head every time I read your article title, since I posted!
(Honestly, though, I know nothing about TypeScript or mongoose, so I can't make any factual or useful comparisons to Python...and anyway, even if I could, it wouldn't be useful, since you already picked the stack you need!)
I moved to Node.JS from Python :)
Hey there you can use typegoose also you can completely remove the double Model/Schema generation and even the DTOs are not required..
Example with nestjs + typegoose + mongodb :
github.com/gochev/nest-js-poc-mongodb
also I have added a blog post about that
dev.to/gochev/nestjs-mongo-typegoo...