[SOLVED] Implement a generic that is an object in TypeScript

Issue

I want to have a onFilterChange helper function that can be used for all filters, so that I dont need to type it for every different filter, but I’m stuck:

// helper.ts
export function onFilterChange(prevState: Record<string, any>, fieldName: string, value: string, isDelete = false): Record<string, any> {
  const newFilters: Record<string, any> = { ...prevState };

  if (isDelete) {
    delete newFilters[fieldName];
  } else {
    newFilters[fieldName] = value;
  }

  if (fieldName !== 'page' && 'page' in newFilters) {
    newFilters.page = '1';
  }

  return newFilters;
}

// index.tsx
const [filters, setFilters] = React.useState({page: 1, order: 'desc'});
setFilters(prevState => onFilterChange(prevState, 'statuses', '', true)); // error

The error for the last line of code above is:
Type ‘Record<string, any>’ is missing the following properties from type ‘{ page: number; order: string; }’: page, order

I’ve also tried T extends Record<string, any> but there is error for this code:

newFilters[fieldName] = value;

Type string cannot be used to index type T.

Appreciate any pointers. Thanks

Solution

The problem with the this answer is that if you assert the type, you will not be able to access the value of filters.statuses later.

You’ll have to type the useState ahead of time

const [filters, setFilters] = React.useState<
  {page: number; order: string; statuses: string}
>({page: 1, order: 'desc'});

And change your function to a generic

//Note this generic only extends page, since this is the only
//value we need guranteed, and access directly with the line below
export function onFilterChange<T extends { page: string }>(
  prevState: T,
  fieldName: keyof T,
  value: T[typeof fieldName],
  isDelete = false
): T {
  const newFilters: T = { ...prevState };

  if (isDelete) {
    delete newFilters[fieldName];
  } else {
    newFilters[fieldName] = value;
  }

  if (fieldName !== 'page' && 'page' in newFilters) {
    newFilters.page = '1'; //Here is why T needs to extend {page: string}
  }

  return newFilters;
}

Alternatively, if you are setting lots of fields but don’t know their type yet you can use the index accessor

const [filters, setFilters] = React.useState<{
  page: number;
  order: string;
} & { [index: string]: string }>({page: 1, order: 'desc'});

We have to use an intersection here, otherwise, we’ll get an error Property 'page' of type 'number' is not assignable to 'string' index type 'string'.ts(2411).

Also note that TS catches an error here you already made, where the page value is a number in your useState, but use reassign it as a string in your function, I don’t know if that is intentional or not, but type/fix as needed.

Answered By – Cody Duong

Answer Checked By – Mary Flores (BugsFixing Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *