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.