1

I have a special case, where I need to write an object configuration for predefined filters. I would like to use typescript generics to accomplish this task.

So, I have the following configuration:

const filterOptions: FilterOption[] = [
  // These should be valid
  { field: 'name', operator: 'sw', operand: 'Mr.' },
  { field: 'age', operator: 'lt', operand: 18 },
  { field: 'joinDate', operator: 'gte', operand: new Date(new Date().setFullYear(new Date().getFullYear() - 1)) },
  
  // These should NOT be valid
  { field: 'name', operator: 'eq', operand: 5 },
  { field: 'age', operator: 'in', operand: 5 },
];

with these types:

interface Filterable {
  name: string;
  age: number;
  joinDate: Date;
}

type NumberOperator = 'lt' | 'lte' | 'gt' | 'gte' | 'eq' | 'ne';
type StringOperator = 'eq' | 'ne' | 'in' | 'ni' | 'sw' | 'ew';

type FilterOption = {
  field: keyof Filterable,

  operator: ???, // What type should I write here? 
                 // NumberOperator type should apply for fields with number and Date types only.
                 // StringOperator type for fields with string type only.

  operand: ???, // What type should I write here?
                // This should be the type of the field as the key in Filterable interface.
}

Two questions:

  1. operand type - It seems to me like it is possible, but I don't know how to write it correctly. This is the closest solution I found, but I can't make it work for objects, this type of generics seems to work only with functions. How should I write generics correctly for this type?
  2. operator type - I think this is much more complicated, and I don't know if it's even possible to accomplish. Is it possible?

Currently I'm using the following way to validate the configuration object. A bit ugly, but works:

interface FilterableString {
  name: string;
}
interface FilterableNumber {
  age: number;
}
interface FilterableDate {
  joinDate: Date;
}
interface Filterable extends FilterableString, FilterableNumber, FilterableDate {}

type FilterOptionString = {
  field: keyof FilterableString,
  operator: StringOperator,
  operand: string,
}
type FilterOptionNumber = {
  field: keyof FilterableNumber,
  operator: NumberOperator,
  operand: number,
}
type FilterOptionDate = {
  field: keyof FilterableDate,
  operator: NumberOperator,
  operand: Date,
}
type FilterOption = FilterOptionString | FilterOptionNumber | FilterOptionDate;

But again, I'm looking for a way to do it using generics, as in the future things might get even uglier and complicated, when more fields and types are added. This may become a bit of a headache to maintain, so I'm trying to avoid that.

1 Answer 1

2

You need to create a union of all allowed states of the object. In other words, make illegal state unrepresentable:

interface Filterable {
    name: string;
    age: number;
    joinDate: Date;
}

type NumberOperator = 'lt' | 'lte' | 'gt' | 'gte' | 'eq' | 'ne';

type StringOperator = 'eq' | 'ne' | 'in' | 'ni' | 'sw' | 'ew';


type GetFilter<T> = T extends string ? StringOperator : T extends number | Date ? NumberOperator : never;


type Values<T> = T[keyof T]

type FilterOption = Values<{
    [Prop in keyof Filterable]:
    { field: Prop, operator: GetFilter<Filterable[Prop]>, operand: Filterable[Prop] }
}>

const filterOptions: FilterOption[] = [
    // These should be valid
    { field: 'name', operator: 'sw', operand: 'Mr.' },
    { field: 'age', operator: 'lt', operand: 18 },
    { field: 'joinDate', operator: 'gte', operand: new Date(new Date().setFullYear(new Date().getFullYear() - 1)) },

    // These should NOT be valid
    { field: 'name', operator: 'eq', operand: 5 },
    { field: 'age', operator: 'in', operand: 5 },
];

Playground

Values - obtains a union of all object values

GetFilter - checks the type of property. If it is a string - return StringOperator, if it is a number or Date - return NumberOperator, otherwise - return never.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks! It did the trick.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.