/mongo_dotType

TypeScript types for using dot notation in mongo queries

Primary LanguageTypeScriptMIT LicenseMIT

mongo_dotType

TypeScript (v4.1+) types for using mongoDB dot notation while querying mongo documents.

How to use

Install

npm install mongo_dottype --save-dev

Use

import { mongoDot_lvl2 } from "mongo_dottype";

interface Users {
    /* ... */
}

// If you don't need array indices for "array element dot notation"
type Users_dotNotation_a = mongoDot_lvl2<Users>;

// If you need some array indices to work, i.e. for the first two elements
type Users_dotNotation_b = mongoDot_lvl2<Users, 0|1>;

// Although allowed, by the TypeScript v4.1.4 type inference for this case is practically not supported (see the bottom section)
type Users_dotNotation_c = mongoDot_lvl2<Users, number>;

Quick example:

Doc1.ts
import { mongoDot_lvl2 } from "mongo_dottype";

export interface Doc1 {
    prop1: string,
    prop2: {
        sp21:  string,
        sp22?: number,
    },
    prop3: number,
    prop4: {
        sp41: string,
        sp42: number,
    },
    prop5: string[];
};


export type Doc1_mongoDot = mongoDot_lvl2<Doc1, 0|1|2>;
//
// the type resolves to:
//
type Doc1_mongoDot = {
    _id?: number;
    __v?: number;
    prop1?: string;
    prop2?: {
        sp21: string;
        sp22?: number;
    };
    "prop2.sp21"?: string;
    "prop2.sp22"?: number;
    prop3?: number;
    prop4?: {
        sp41: string;
        sp42: number;
    };
    "prop4.sp41"?: string;
    "prop4.sp42"?: number;
    prop5?: string[];
    "prop5.0"?: string;
    "prop5.1"?: string;
    "prop5.2"?: string;
}

Longer example:

Doc2.ts
export interface Doc2 {
    prop1: string,
    prop2: {
        sp21: string,
        sp22: number,
        sp23: {
            sp23el_prop1: number,
            sp23el_prop2?: string,
        }[],
        sp24?: {
            ssp241: string,
            ssp242?: boolean,
        }
    },
    prop3: number,
    prop4: {
        sp41: string,
        sp42: number,
    },
    prop5: string[];
    prop6: {
        prop6el_prop1: string,
        prop6el_prop2?: number,
    }[];
    prop7?: string[];
};


export type Doc2_mongoDot = mongoDot_lvl2<Doc2, 0|1|2>;
//
// the type resolves to:
//
type Doc2_mongoDot = {
    _id?: number;
    __v?: number;
    prop1?: string;
    prop2?: {
        sp21: string;
        sp22: number;
        sp23: {
            sp23el_prop1: number;
            sp23el_prop2?: string;
        }[];
        sp24?: {
            ssp241: string;
            ssp242?: boolean;
        };
    };
    "prop2.sp21"?: string;
    "prop2.sp22"?: number;
    "prop2.sp23"?: {
        sp23el_prop1: number;
        sp23el_prop2?: string;
    }[];
    "prop2.sp23.0"?: {
        sp23el_prop1: number;
        sp23el_prop2?: string;
    };
    "prop2.sp23.1"?: {
        sp23el_prop1: number;
        sp23el_prop2?: string;
    };
    "prop2.sp23.2"?: {
        sp23el_prop1: number;
        sp23el_prop2?: string;
    };
    "prop2.sp24"?: {
        ssp241: string;
        ssp242?: boolean;
    };
    "prop2.sp24.ssp241"?: string;
    "prop2.sp24.ssp242"?: boolean;
    prop3?: number;
    prop4?: {
        sp41: string;
        sp42: number;
    };
    "prop4.sp41"?: string;
    "prop4.sp42"?: number;
    prop5?: string[];
    "prop5.0"?: string;
    "prop5.1"?: string;
    "prop5.2"?: string;
    prop6?: {
        prop6el_prop1: string;
        prop6el_prop2?: number;
    }[];
    "prop6.0"?: {
        prop6el_prop1: string;
        prop6el_prop2?: number;
    };
    "prop6.0.prop6el_prop1"?: string;
    "prop6.0.prop6el_prop2"?: number;
    "prop6.1"?: {
        prop6el_prop1: string;
        prop6el_prop2?: number;
    };
    "prop6.1.prop6el_prop1"?: string;
    "prop6.1.prop6el_prop2"?: number;
    "prop6.2"?: {
        prop6el_prop1: string;
        prop6el_prop2?: number;
    };
    "prop6.2.prop6el_prop1"?: string;
    "prop6.2.prop6el_prop2"?: number;
    prop7?: string[];
    "prop7.0"?: string;
    "prop7.1"?: string;
    "prop7.2"?: string;
}

Deepness and performance

The package provides only with types for subproperties up to 2 levels deep, i.e.:

// from
{
    prop2: {
        sp24: {
            ssp241: string;
        };
    };
}
// to
{
    "prop2.sp24.ssp241"?: string;
}

But you can still define deeper levels yourself (3rd, 4th, maybe 5th) by copying commented definitions from the mongoDotType.type.d.ts file.

These definitions were left commented intentionally though.

Starting with level 3, simply having such high level type alias definition can cause TS compiler to run slower.

Starting with level 4, in some cases TypeScript fails to instantiate a type producing the error: Type instantiation is excessively deep and possibly infinite. ts(2589)

You are unlikely to make level 5 work at all. If you do, more power to you!


Even longer example (using mongoose):

Say you create a mongoose model:

Doc3.model.ts
import { Document, model, Schema } from "mongoose";


export interface Doc3 {
  prop1: string,
  prop2: {
      sp21: string,
      sp22?: number,
      sp23: {
          ssp23el_prop1: number,
          ssp23el_prop2?: string,
      }[],
  },
  prop3: number,
  prop4: {
      sp41: string,
      sp42: number,
      sp43: number[];
      sp44: {
          ssp441: number,
          ssp442: string,
          ssp443: {
              sssp4431: string,
              sssp4432: number,
              sssp4433: number,
          },
      }
  },
  prop5: string[];
  prop6: {
      prop6el_prop1: string,
      prop6el_prop2?: number,
      prop6el_prop3?: {
          prop6el_prop3el_prop1: string,
          prop6el_prop3el_prop2: number,
          prop6el_prop3el_prop3: {
              prop6el_prop3el_sp31: number,
              prop6el_prop3el_sp32: string,
              prop6el_prop3el_sp33: string[],
          },
      }[],
  }[];
  prop7?: string[];
};


const Doc3Schema = new Schema({
  prop1: { type: String, required: true, unique: false, },
  prop2: {
      type: {
          sp21: { type: String, required: true, unique: false, },
          sp22: { type: Number, required: false, unique: false, },
          sp23: {
              type: [{
                  ssp23el_prop1: { type: Number, required: true, unique: false, },
                  ssp23el_prop2: { type: String, required: false, unique: false, },
              }],
              required: true, unique: false,
          },
      },
      required: true, unique: false,
  },
  prop3: { type: Number, required: true, unique: false, },
  prop4: {
      type: {
          sp41: { type: String, required: true, unique: false, },
          sp42: { type: Number, required: true, unique: false, },
          sp43: { type: [Number], required: true, unique: false, },
          sp44: {
              type: {
                  ssp441: { type: Number, required: true, unique: false, },
                  ssp442: { type: String, required: true, unique: false, },
                  ssp443: {
                      type: {
                          sssp4431: { type: String, required: true, unique: false, },
                          sssp4432: { type: Number, required: true, unique: false, },
                          sssp4433: { type: Number, required: true, unique: false, },
                      },
                      required: true, unique: false,
                  },
              },
              required: true, unique: false,
          },
      },
      required: true, unique: false,
  },
  prop5: { type: [String], required: true, unique: false, },
  prop6: {
      type: [{
          prop6el_prop1: { type: String, required: true, unique: false, },
          prop6el_prop2: { type: Number, required: false, unique: false, },
          prop6el_prop3: {
              type: [{
                  prop6el_prop3el_prop1: { type: String, required: true, unique: false, },
                  prop6el_prop3el_prop2: { type: Number, required: true, unique: false, },
                  prop6el_prop3el_prop3: {
                      type: {
                          prop6el_prop3el_sp31: { type: Number, required: true, unique: false, },
                          prop6el_prop3el_sp32: { type: String, required: true, unique: false, },
                          prop6el_prop3el_sp33: { type: [String], required: true, unique: false, },
                      },
                      required: true, unique: false,
                  },
              }],
              required: false, unique: false,
          },
      }],
      required: true, unique: false,
  },
  prop7: { type: [String], required: false, unique: true, },
});


export type Doc3_document = Doc3 & Document;

export default model<Doc3_document>('Doc3', Doc3Schema);

Define the type generic for dot notation up to 3 lvl deep:

mongoDot_deeper.ts
import { mongoDot_lvl2, obj_defaultDocFields, sub, sublvl0, sublvl1, sublvl2 } from "mongo_dottype";
import { Expand, FlattenUnion_shallow, objNotNever } from "mongo_dottype/dist/extras.type";

/**
* Picks subproperties (& subelements) of 3rd level deep (subsubsub-properties and -elements) with dot notation
* If `arrIndices` is provided, uses all these indices to pick elements of array properties of the given object with dot notation
*/
export type sublvl3<T, arrIndices extends number = never> = objNotNever<sub<sublvl2<T, arrIndices>, arrIndices>>; 
/**
* Converts into a type with subproperties and subelements of up to 3 levels deep with dot notation
* If `arrIndices` is provided, uses all these indices to pick elements of array properties of the given object with dot notation
*/
export type mongoDot_lvl3<T, arrIndices extends number = never> = Expand<FlattenUnion_shallow<obj_defaultDocFields | sublvl0<T, arrIndices> | sublvl1<T, arrIndices> | sublvl2<T, arrIndices> | sublvl3<T, arrIndices>>>;

Then, the recommended usage by default would be the following:

FindDocs3.ts
import Doc3Model, { Doc3 } from "./Doc3.model";
import { FilterQuery } from "mongoose";

export async function FindDocs3(filter: FilterQuery<Required<mongoDot_lvl3<Doc3, 0|1|2>>>, select?: string): Promise<Doc3[]> {
    return Doc3Model.find(filter).select(select).exec();
}
queryDoc3_example.ts
import { mongoDot_lvl3 } from "./mongoDot_deeper";
import { FindDocs3 } from "./FindDocs3.ts";


(async () => {
    const DB = await DBconnection;



    ///// Test 1 (OK) /////
    (Doc3Model as Model<mongoDot_lvl3<Doc3, 0> & Document>).updateMany({
        "prop4.sp43.0": {
            $in: [0, 1],
        }
    },
    {
        $push: {
            "prop2.sp23": {
                ssp23el_prop1: 3,
                ssp23el_prop2: 'foobar'
            },
            "prop4.sp43": {
                $each: [2, 3, 4]
            },
        }
    }).exec();
    
    
    
    ///// Test 2 (ERROR) /////
    (Doc3Model as Model<mongoDot_lvl3<Doc3, 0> & Document>).updateMany({
        "prop4.sp43.0": {
            $in: [0, 1],
        }
    },
    {
        $push: {
            "prop2.sp23": {
                ssp23el_prop1: 3,
                ssp23el_prop2: 'foobar'
            },
            "prop4.sp43": {
                $each: [2, 3, "four"] // <-- Type 'string' is not assignable to type 'number'. ts(2322)
            },
        }
    }).exec();
    
    
    
    ///// Test 3 (OK) /////
    FindDocs3({
        "prop6.0.prop6el_prop3.2": {
            $ne: {
                prop6el_prop3el_prop1: "foo",
                prop6el_prop3el_prop2: 100,
                prop6el_prop3el_prop3: {
                    prop6el_prop3el_sp31: 200,
                    prop6el_prop3el_sp32: "BAR",
                    prop6el_prop3el_sp33: [],
                }
            }
        }
    });
    
    
    
    ///// Test 4 (ERROR) /////
    FindDocs3({
        "prop6.0.prop6el_prop3.2": {
            $ne: {
                prop6el_prop3el_prop1: "foo",
                prop6el_prop3el_prop2: 100,
                prop6el_prop3el_prop3: {
                    prop6el_prop3el_sp31: 200,
                    prop6el_prop3el_sp32: [], // <-- Type 'undefined[]' is not assignable to type 'string'. ts(2322)
                    prop6el_prop3el_sp33: [],
                }
            }
        }
    });
    
    
    

    return await DB.disconnect();
})();

Using number type for allowed array indices

This type alias:

type Users_dotNotation = mongoDot_lvl2<Users, number>;

will result in a type with object keys of type `somestring${number}`.

If you explicitly try to use this type literal (`prop.${number}`) to index an object, TSC may produce an error: "`prop.${number}` cannot be used to index type ...". But when calculated for the type aliases provided here, such a situation, instead of an error, "silently" produces either type never or, weirdly enough, undefined in place of an object that you're trying to index.

By the time I'm writing it, using number practically doesn't work. Although the TS calculations seem to work out correctly, the key part, the type checking, simply isn't supported for type literals as object keys by now. You can play around with the PickSubs_dotNotation type, as this is where (almost) all the magic happens. I will be submitting a GitHub issue to TypeScript regarding usage of somestring${number} for indexing objects.