Home > Mobile >  Implement a generic that is an object in TypeScript
Implement a generic that is an object in TypeScript

Time:03-21

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

CodePudding user response:

You can apply a type assertion like this:

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

CodePudding user response:

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.

  • Related