reactjstypescripttypesnext.jsindex-signature

Typescript index signature error : Type error: Argument of type is not assignable to parameter of type 'SetStateAction<never[]>'


I'm new to typescript and to Index signatures and I don't find how to solve that error in my code. I assume that I need to something like on variable sortProperty on the variable sorted, but I can't find the answer. I have 2 errors. One on the setData(sorted)

Argument of type { id: number; jobName: string; created: string; modified: string; }[]' is not assignable to parameter of type 'SetStateAction<never[]>'

and the other on (b[sortProperty])

Element implicitly has an 'any' type because expression of type 'string' can't be used to index

data.js

export const tasks = [
    {
      id: "1",
      jobName: 'MK-00544 • Spacecraft',
      created: '2016-09-03',
      modified: '2016-09-04'
    },
    {
      id: "2",
      jobName: 'MK-00545 • Spacecraft',
      created: '2017-09-26',
      modified: '2018-09-29'
    },
    {
      id: "3",
      jobName: 'MK-00546 • Spacecraft',
      created: '2019-10-03',
      modified: '2022-12-08'
    },
    {
      id: "4",
      jobName: 'MK-00547 • Spacecraft',
      created: '2020-09-12',
      modified: '2021-09-03'
    },
    {
      id: "5",
      jobName: 'MK-005478 • Spacecraft',
      created: '2019-09-03',
      modified: '2019-09-03'
    },
    {
      id: "6",
      jobName: 'MK-00549 • Spacecraft',
      created: '2019-09-03',
      modified: '2019-09-03'
    }
  ]

Job.tsx :

 interface Task {
    [key: string]: string
  }
  const [data, setData] = useState<Task>([])
  const [sortType, setSortType] = useState('jobName')

  useEffect(() => {
    const sortArray = (type: string) => {
      interface StringIndexedObject {
        [key: string]: string
      }
      const types: StringIndexedObject = {
        jobName: 'jobName',
        created: 'created',
        modified: 'modified'
      }
      interface AnotherStringIndexedObject {
    [key: string]: keyof Task
  }
  const sortProperty = types[type]
  const sorted: AnotherStringIndexedObject = [...tasks].sort(
    (a, b) => new Date(b[sortProperty]) - new Date(a[sortProperty])
  )
      console.log(sorted)
      setData(sorted)
    }

    sortArray(sortType)
  }, [sortType])
  return (
<div className="font-bold">Sort by:</div>
            <select
              onChange={(e) => setSortType(e.target.value)}
              className="text-center px-5 bg-gray-200 rounded-xl"
            >
              <option value="id">Id</option>
              <option value="jobName">Job name</option>
              <option value="created">Newest</option>
              <option value="modified">Last modified</option>
            </select>
            <div>({tasks.length} users)</div>

   {data.map((task) => (
                <tr
                  key={task.id}
                  className="tableau text-center cursor-pointer"
                  onClick={() => router.push(`/job/${task.id}`)}
                >
                  <td className="py-3">{task.jobName}</td>
                  <td className="py-3">
                    <div className="flex items-center gap-5 justify-center">
                      {task.created}
                    </div>
                  </td>
                  <td className="py-3">
                    <div className="flex items-center gap-5 justify-center">
                      {task.modified}
                    </div>
                  </td>
              ))}

Solution

  • You've got a bunch of issues here which are all related. When the type for one variable is incomplete or incorrect it can cause issues in other places where you use that variable.

    Your component's job is to sort and render an array of task objects. It internally stores two states:

    1. The current sort property.
    2. The array of sorted tasks.

    (Note: the sorted array one doesn't need to be a state as it is a derived value. Personally I would use a useMemo instead of a useState and a useEffect but what you have will work so let's keep it and focus on the TypeScript issues)

    Here are the things that I've fixed:

    1. Describe the type of a task object.

    Not every string is a valid key here. We have a few known properties and that's it.

    interface Task {
        id: string;
        jobName: string;
        created: string;
        modified: string;
    }
    

    2. Set a type for the data state.

    It is an array of tasks aka Task[]. You need a type here because the initial value is an empty array which has type never[]. If the initial array was not empty then it could be inferred automatically.

    const [data, setData] = useState<Task[]>([])
    

    3. Defined your types object as mapping from string to keyof Task.

    Now the sortProperty has the type keyof Task, meaning that it is always a property of the Task.

    const sortArray = (type: string) => {
        interface StringIndexedObject {
            [key: string]: keyof Task
        }
        const types: StringIndexedObject = {
            jobName: 'jobName',
            created: 'created',
            modified: 'modified'
        }
        const sortProperty = types[type];
    

    You can also do:

    const types: Record<string, keyof Task> = {
    

    4. Removed the AnotherStringIndexedObject type.

    The sorted array is not an object. It is an array Task[]. You can write const sorted: Task[], but you don't need to.

    5. Removed the new Date handling.

    This doesn't make sense if the sort type is 'jobName' or 'id'. What is new Date('MK-005478 • Spacecraft')? For the fields which are dates, we can compare the ISO strings as strings.

    6. Used lodash sortBy for the sorting.

    You can define a custom compare function but I find this really annoying.

    const sorted = sortBy(tasks, sortProperty);
    

    7. Renamed your data file from data.js to data.ts

    If it is a TypeScript file then your imported tasks variable will have proper types based on its values.


    Job.tsx

    import React, { useState, useEffect } from 'react';
    import { sortBy } from 'lodash';
    import { useRouter } from 'next/router';
    import { tasks } from './data';
    
    export interface Task {
        id: string;
        jobName: string;
        created: string;
        modified: string;
    }
    
    export const Job = () => {
        const router = useRouter();
    
        const [data, setData] = useState<Task[]>([])
        const [sortType, setSortType] = useState('jobName')
    
        useEffect(() => {
            const sortArray = (type: string) => {
                interface StringIndexedObject {
                    [key: string]: keyof Task
                }
                const types: StringIndexedObject = {
                    jobName: 'jobName',
                    created: 'created',
                    modified: 'modified'
                }
                const sortProperty = types[type];
                const sorted = sortBy(tasks, sortProperty);
                console.log(sorted)
                setData(sorted)
            }
    
            sortArray(sortType)
        }, [sortType]);
    
        return (
            <div>
                <div className="font-bold">Sort by:</div>
                <select
                    onChange={(e) => setSortType(e.target.value)}
                    className="text-center px-5 bg-gray-200 rounded-xl"
                >
                    <option value="id">Id</option>
                    <option value="jobName">Job name</option>
                    <option value="created">Newest</option>
                    <option value="modified">Last modified</option>
                </select>
                <div>({tasks.length} users)</div>
    
                {data.map((task) => (
                    <tr
                        key={task.id}
                        className="tableau text-center cursor-pointer"
                        onClick={() => router.push(`/job/${task.id}`)}
                    >
                        <td className="py-3">{task.jobName}</td>
                        <td className="py-3">
                            <div className="flex items-center gap-5 justify-center">
                                {task.created}
                            </div>
                        </td>
                        <td className="py-3">
                            <div className="flex items-center gap-5 justify-center">
                                {task.modified}
                            </div>
                        </td>
                    </tr>
                ))}
            </div>
        )
    }