A guide to leveraging TypeScript generics for accurate validation of object values

In a unique scenario, I am tasked with creating an object configuration for predefined filters using TypeScript generics.

Here is the current configuration:

const filterOptions: FilterOption[] = [
  // Valid filters
  { 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)) },
  
  // Invalid filters
  { field: 'name', operator: 'eq', operand: 5 },
  { field: 'age', operator: 'in', operand: 5 },
];

Here are the types involved:

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: ???, 
  operand: ???,
}

I have two main questions:

  1. operand type - I need help defining this correctly using generics.
  2. operator type - Is it possible to achieve this level of complexity with generics?

Currently, I am using a workaround to validate the configuration object, but I am seeking a cleaner solution using generics

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;

I aim to simplify the process with generics to handle potential future complexities and maintainability challenges.

Answer №1

To ensure the object's states are all allowed, you must create a union. The goal is to make illegal states impossible to represent:

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 - retrieves a union of all object values

GetFilter - validates the property type. If it's a string, it returns StringOperator; if it's a number or Date, it returns NumberOperator; otherwise, it returns never.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What is the best way to invoke a function in a React component from another component?

I am working with a component structure that includes the Input component. In this component, there is a function called validate that is triggered by the onChange event. Here is a snippet of the code: https://i.sstatic.net/WjCLy.png import React, {FC, us ...

Monitor the input value for any changes in Angular 8 using the listen component

Hey there! I'm currently working with a component that includes the input @Input() userId: number[] = []; to receive a list of user IDs. Specifically, I have integrated this component into another one, such as the news component: <kt-user-post-li ...

Tips for sending a query using the http GET method in Next.JS 14 API routes

When using the Next.js 14 API route, I am passing a page in the GET method to paginate the data fetched from my database. However, an error is thrown when trying to retrieve the query from the request: Property 'query' does not exist on type &a ...

A guide on implementing code sharing in NestJS using Yarn Workspaces

I'm currently working on a proof of concept for a basic monorepo application. To structure my packages, I've decided to use Yarn Workspaces instead of Lerna as it seems more suitable for my needs. One of the packages in my setup is shared, which ...

Encountering browser freezing issues with a Next.JS app when trying to add an input field

I am currently utilizing Next.JS to construct a form with two inputs. I have followed the traditional React approach for input text reading and validation. "use client" import { firebaseApp } from '@/app/firebase'; import React, { useCa ...

Transfer Typescript Project to Visual Studio Code

When I first started my project, I used the Typescript HTML Application Template project template. It worked well and set up a project for me. However, now I want to transition to using VSCode. The issue I'm facing is figuring out which switches and c ...

The configuration file tsconfig.json did not contain any input

After downloading angular2-highcharts through npm for my application, I encountered an error in the tsconfig.json file of the package while using Visual Studio Code: file: 'file:///c%3A/pdws-view-v2/node_modules/angular2-highcharts/tsconfig.json&apos ...

Is the RouterModule exclusively necessary for route declarations?

The Angular Material Documentation center's component-category-list imports the RouterModule, yet it does not define any routes or reexport the RouterModule. Is there a necessity for importing the RouterModule in this scenario? ...

The process of inserting data into MongoDB using Mongoose with TypeScript

Recently, I encountered an issue while trying to insert data into a MongoDB database using a TypeScript code for a CRUD API. The problem arises when using the mongoose package specifically designed for MongoDB integration. import Transaction from 'mon ...

Implementing handleRequest as an asynchronous function within the passportjs guard

@Injectable() export class RefreshAuthGuard extends JwtAuthGuard { constructor( private readonly jwtService: JwtService, ) { super(); } public handleRequest(err: any, user: any, info: Error, ctx: any): any { if (err ...

Effectively managing intricate and nested JSON objects within Angular's API service

As I work on creating an API service for a carwash, I am faced with the challenge of handling a large and complex json object (referred to as the Carwash object). Each property within this object is essentially another object that consists of a mix of simp ...

Retrieve the essential information needed from the REST API

I have a test wordpress blog set up. To enhance the functionality, I developed an angular app that utilizes the wordpress rest api. The app makes a call to an endpoint to retrieve categories. However, the JSON response contains unnecessary data for my appl ...

Exploring the world of chained JavaScript Promises for automatic pagination of an API

Dealing with a paged API that requires fetching each page of results automatically has led me to construct a recursive promise chain. Surprisingly, this approach actually gives me the desired output. As I've tried to wrap my head around it, I've ...

Storing data from a collection of interface objects in a string array

Take a look at the following code snippet: import React, {FC} from 'react'; import {useFetchErrors} from "../Api/Api"; import {useLocation} from "react-router-dom"; interface ExecutionTableProps { project_id: number } const ...

How to reveal hidden Div element at a specific index with Angular Material table

In my mat-table, there are several functionalities available: 1. The ability to add or remove rows 2. Adding data into a row using different controls such as combo-boxes, text boxes, etc. One of the controls is a text box labeled "Additional Information ...

Optimizing Angular for search engines: step-by-step guide

Regarding Angular SEO, I have a question about setting meta tags in the constructors of .ts files. I have implemented the following code: //To set the page title this.titleServ.setTitle("PAGE TITLE") //To set the meta description this.meta.addTag ...

Typescript: Potential null object error when defining a method

I recently encountered an error message stating "Object is possibly null" while working on the changePageSize method in book-store.component.html. It seems like I need to initialize the object within the class, but I'm not familiar with how to do that ...

Creating a service instance within the constructor in Angular 2

I'm new to Angular 2 and Typescript and I'm trying to wrap my head around DI. One thing that has been tripping me up is the way variables referring to a service are typed into the constructor in all the code examples I've come across. Why is ...

Transform Firestore JSON data into a TypeScript array

Extracting and formatting data from Firebase for visualization purposes can be challenging after successfully working with it in HTML. I am currently dealing with a FirebaseListObservable that contains three value-types, but only one of them needs to be in ...

Angular 2 signal sender

I have a specific class definition for my Project: export class Project { $key: string; file: File; name: string; title: string; cat: string; url: string; progress: number; createdAt: Date = new Date(); constructor(file: File) { th ...