1

I have an application that queries a MongoDB database, but is currently only capable of querying a single field in the document:

export interface IKeyValue {
    [property : string] : any ;
}

export interface IFilter {
    [property : string] : IKeyValue;
}

const filter : IFilter = {
    "Name" : { $eq : "Bob" }
}

What I want to do is allow IFilter to allow more than one property:

const filter : IFilter = {
    "Name" : { $eq : "Bob" },
    "Age"  : { $gte : 21 }
}

How can I create a Typescript interface that will allow more than one dynamic key, without reverting to using object or any as the type for IFilter?

Jason
  • 3,943
  • 12
  • 64
  • 104

2 Answers2

2

How I understand your need is:

  • you have some entity, lets say User
  • you want to create a generic Filter type which can be used for different entities
  • generic Filter type should be type safe for every entity

First of all lets define possible rules we need to filter

type Rule<T> = { $eq: T } | { $gte: T } // append any other rules you need

I have taken from your example two rules - $eq and $gte but by | you can add others you need.

Second lets define generic Filter type

type Filter<Obj> = {
    [K in keyof Obj]: Rule<Obj[K]>
}

Our type says - I should have all keys of given object, and for every key I should define a rule which works on the same type as this property has. So for property a: string the rule needs to be or {$eq: string} or {$gte: string}.

Lets see an example:

// example filter for the User type
type User = {
    id: number;
    name: string;
}

// create an instance of the filter for the User entity
const userFilter: Filter<User> = {
    id: { $gte: 1 }, // rule needs to have value as number
    name: {$eq: '2'} // rule needs to have value as string
}

// what if we want to filter by only part of the interface - use Pick utility type
const userFilter2: Filter<Pick<User, 'name'>> = {
    name: {$eq: '2'} // only name is needed to be provided
}

Type Filter is fully type safe, by creating instance of this type we need to define proper keys and proper rules for these keys.

As an addition you can conditionally say which rules are possible for which types. Lets say $gte can be applicable only for numbers, but not for other types. You can do so by:

type Rule<T> = T extends number ? { $eq: T } | { $gte: T } : { $eq: T }

Above definition will prevent from using $gte for anything other than number.

Maciej Sikora
  • 19,374
  • 4
  • 49
  • 50
0

What I want to do is allow IFilter to allow more than one property

IFilter is a type with a string index signature and already allows multiple properties with string name type and IKeyValue value type. It is just that the IDE/compiler cannot assist with auto completion, as it cannot know, what is inside due to the dynamic way the properties can be added.

For example your code would be even valid for unknown property names:

const filterValue = filter["helloWorld"] // IKeyValue
const keyValue = filter["hello"]["World"] // any

I am not sure, what is the best solution here. If there is a set of fix (potentially optional) property names for IFilter like Name or Age, maybe a hybrid type with index signature and known properties can be created:

export interface IFilter {
  [property: string]: IKeyValue | undefined;
  Name?: IKeyValue;
  Age?: IKeyValue;
}

const filter : IFilter = {
  "Name": { $eq: "Bob" } // code assist for Name/Age properties
}
ford04
  • 66,267
  • 20
  • 199
  • 171