Skip to content

Ditch AnyKeys in Model.create and Model.insertOne #15355

Open
@WillsterJohnson

Description

@WillsterJohnson

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the feature has not already been requested

🚀 Feature Proposal

Only reference I can find to this is #11148, which is now several years old.

I initially suggested this be a feature in typegoose (see here), but was informed that mongoose now has the features necessary to infer enough type information to properly type Model.create and Model.insertOne.

I mention in the issue linked above that AnyKeys destroys type information, and is equivalent to the following type, which only preserves keys and not any information about their values, ie the use of T[P] | any is equivalent to any.

// given type
type AnyKeys<T> = { [P in keyof T]?: T[P] | any };
// equivalent to
type AnyKeys<T> = Partial<Record<keyof T, any>>

Likewise, if you put this type in union as T | AnyKeys<T> (as is default in Model.create), it is equivalent to AnyKeys<T>, as typescript favors the least restrictive type in the union if types overlap.

In other words, TRawDocType | AnyKeys<TRawDocType> means Partial<Record<keyof TRawDocType, any>>.

Motivation

Suppose you're storing something of the following shape;

{
  foo: {
    bar: string
    baz: number
  }
}

foo is a required property. It must be an object. It must have the key bar of type string, and it must have the key baz of type `number.

It is unhelpful for the type inference in the first argument to model.create to propose the shape;

{
  foo?: any
}

foo is an optional property. It can be anything at all.

The only way to get around this is to explicitly specify the type on every call to model.create or model.insertOne. This should not be needed. The correct type is already stored in the model via TRawDocType, specifying it again on every call is redundant.

Example

export interface Model/*...*/ {
  //...
-  create<DocContents = AnyKeys<TRawDocType>>(docs: Array<TRawDocType | DocContents>, options: CreateOptions & { aggregateErrors: true }): Promise<(THydratedDocumentType | Error)[]>;
-  create<DocContents = AnyKeys<TRawDocType>>(docs: Array<TRawDocType | DocContents>, options?: CreateOptions): Promise<THydratedDocumentType[]>;
-  create<DocContents = AnyKeys<TRawDocType>>(doc: DocContents | TRawDocType): Promise<THydratedDocumentType>;
-  create<DocContents = AnyKeys<TRawDocType>>(...docs: Array<TRawDocType | DocContents>): Promise<THydratedDocumentType[]>;
+  create(docs: Array<TRawDocType>, options: CreateOptions & { aggregateErrors: true }): Promise<(THydratedDocumentType | Error)[]>;
+  create(docs: Array<TRawDocType>, options?: CreateOptions): Promise<THydratedDocumentType[]>;
+  create(doc: TRawDocType): Promise<THydratedDocumentType>;
+  create(...docs: Array<TRawDocType>): Promise<THydratedDocumentType[]>;
  //...
-  insertOne<DocContents = AnyKeys<TRawDocType>>(doc: DocContents | TRawDocType, options?: SaveOptions): Promise<THydratedDocumentType>;
+  insertOne(doc: TRawDocType, options?: SaveOptions): Promise<THydratedDocumentType>;
}

Or, if it is still the case that TRawDocType is unreliable for whether a property is optional, Partial<TRawDocType> could be used. If its unreliable for nested keys too, a DeepPartial type (eg, ts-essentials DeepPartial) could be created and used.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions