Home > Blockchain >  How to access $and operator from Filter<T> in Typescript, in method where Filter<T> is p
How to access $and operator from Filter<T> in Typescript, in method where Filter<T> is p

Time:10-17

I have some code which works, but keeps on throwing up TypeScript errors in my IDE. I have condensed the code down to show what my issue is.

Within a Next.JS app, I have a type (TestModel), a method which is used to build a massive query on that model (generateTestFilter), and further methods which handle the logic of each filter condition (handleNameFilter). When trying to access the $and operator within the handleNameFilter, TypeScript doesn't like this for some reason, whereas if I try to access it in the method generateTestFilter, there are no issues here. Any idea why this happens?

import { Filter } from "mongodb"

export interface TestModel {
    name: string,
    things: string[],
    stuff: number
}

function generateTestFilter(tm: TestModel) {
    const filter: Filter<TestModel> = { $and: [] }

    handleNameFilter(tm, filter)

    if (filter.$and && filter.$and.length) { // $and can be accessed here with no issues
        return filter
    }
    return null
}

function handleNameFilter(tm: TestModel, filter: Filter<TestModel>) {
    const condition: Filter<TestModel> = {name: 'Steve'}
    filter.$and.push(condition) // Property '$and' does not exist on type 'Filter<TestModel>'. Property '$and' does not exist on type 'Partial<TestModel>'.ts(2339)

}

TypeScript Playground

CodePudding user response:

Problem

The definition for Filter looks like this:

/** A MongoDB filter can be some portion of the schema or a set of operators @public */
export type Filter<TSchema> =
  | Partial<TSchema>
  | ({
      [Property in Join<NestedPaths<WithId<TSchema>, []>, '.'>]?: Condition<
        PropertyType<WithId<TSchema>, Property>
      >;
    } & RootFilterOperators<WithId<TSchema>>);

node-mongodb-native/src/mongo_types.ts:67-74 @ 5f37cb6

You can see it's a union of two types with the following meaning:

  1. All the properties on TSchema made optional.
  2. A mapped type (too long to explain) intersected with the root filter operators (of which $and is a valid property).

Why the error in handleNameFilter?

As the type is a union, by default, you can only access properties/methods that are valid for every member of the union. In handleNameFilter you attempt to access $and. This property only appears in the second half of the union but does not appear in the first half (Partial<TestModel>). As a result, TypeScript raises an error.

Why no error in generateTestFilter?

Previously, I've written:

As the type is a union, by default, you can only access properties/methods that are valid for every member of the union.

However you can narrow the type to one side of the union. Narrowing is giving TypeScript information to help make a type more specific (aka narrow). In generateTestFilter you've done this here:

const filter: Filter<TestModel> = { $and: [] }

It's actually not significant that the property name in the object assigned to filter is $and. What is significant is that the property name is not one that appears in TestModel. As none of the properties match the properties of TestModel, it doesn't satisfy the first half of the union (Partial<TestModel>) so TypeScript narrows it to the second half of the union. You can test this by changing the key/value to match properties in TestModel – this should raise the error.

Solution

Now that the problem has been explained, we should be able to move to a solution. At first glance, it may seem the answer is to somehow narrow the type. However there's an additional problem I'd like to get to, which I think is the actual root cause.

function handleNameFilter(tm: TestModel, filter: Filter<TestModel>) {
    const condition: Filter<TestModel> = {name: 'Steve'}
    filter.$and.push(condition) // Property '$and' does not exist on type 'Filter<TestModel>'. Property '$and' does not exist on type 'Partial<TestModel>'.ts(2339)

}

Your handleNameFilter function takes in an argument at the filter parameter that is typed to Filter<TestModel> then makes a push to the $and property on filter. However there's no guarantee that $and will exist in the first place. $and only exists because of this:

    const filter: Filter<TestModel> = { $and: [] }

    handleNameFilter(tm, filter)

Hypothetically, in the future, you may refactor this and remove the first line. This will break your logic. TypeScript is protecting you from this.

As a result, you'll somehow need to make $and available to push to. The options are either:

  1. Enforce the existence of the $and property in the type of the filter parameter. Although, I would not recommend this because the responsibility of adding the property would be better placed within the function. Therefore:
  2. Add the $and property to filter as part of the logic in your function.

CodePudding user response:

Besides @Wing detailed explanations, implementing the solution actually requires a bit of both "options":

  • As described in point 2, make sure the $and operator is also initialized within your handleNameFilter function (filter argument being a Filter, it may not have it yet)
  • We still need a bit of point 1 (i.e. enforcing the existence of $and member in parameter), because of the union in Filter: in case the argument is a plain Partial<TestModel>, it does not expect adding later on that $and operator, i.e. TypeScript does not like when we "transform" the type to use the other part of the union
function handleNameFilter(
    tm: TestModel,
    // Force TS to accept adding later on the $and operator,
    // otherwise it complains because the Partial<TSchema> part of the union
    // does not expect it.
    filter: Filter<TestModel> & { $and?: Filter<TestModel>[] }
) {
    const condition: Filter<TestModel> = { name: 'Steve' }

    // Make sure the $and operator is present and initialized
    filter.$and ??= []

    filter.$and.push(condition) // Okay
}

Playground Link

  • Related