1

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.

atomAltera
  • 1,702
  • 2
  • 19
  • 38
  • If you're referring to the fact that some properties are duplicated, then why not create a higher order interface with the dups? because you could technically do something like `interface Tariff extends UserInfo` where `UserInfo` is an interface with everything regarding users. – kemicofa ghost Feb 28 '19 at 10:47
  • I tried this. And this become to some messy hello of `VehicleCommonFields`, `VehicleModelCommonFields` and so on. Since some fields can have different types for model and for entity. F.e. I can have some field `foo` that in `Vehicle` entity has type `A`, and in model type it is serialized to `string`. – atomAltera Feb 28 '19 at 10:54
  • 2
    https://stackoverflow.com/questions/49228020/creating-mongoose-models-with-typescript-subdocuments/55194560#55194560 i hope this can help you – aoi umi Mar 16 '19 at 08:30

0 Answers0