Argument of type 'InputType[]' is not assignable to parameter of type 'GenericType[]' Typescript

Viewed 358

my intent here is to write a helper function whose aim is to dynamically sort an array of objects (first argument) in alphabetical order, based on a key passed as second argument.

The function:

interface GenericObject {
  [key: string]: string;
}

export const sortAlphabetically = (array: Array<GenericObject>, sortBy: keyof GenericObject) => {
  let isKeyValid = false;
  
  // check the key exists in the object before attempting to sort by it
  if (array.length > 0) {
    isKeyValid = array.every(obj => Object.prototype.hasOwnProperty.call(obj, sortBy)  && typeof obj[sortBy] === 'string');
  }

  if (isKeyValid) {
    array.sort((a: GenericObject, b: GenericObject) =>
      a[sortBy].toLowerCase() < b[sortBy].toLowerCase()
        ? -1
        : a[sortBy].toLowerCase() > b[sortBy].toLowerCase()
        ? 1
        : 0,
    );
    return array;
  } else {
    return;
  }
};

So now, even before being able to test my function, if I try to do this:

export interface Person {
  name: string;
  surname: string;
} 

const people: Person[] = [
{name: 'John', surname: 'Smith'},
{name: 'Tony', surname: 'Denver'},
{name: 'Mary', surname: 'Howard'},
]

sortAlphabetically(people, 'name');

or this:

export interface Car {
  model: string;
  make: string;
} 

const cars: Car[] = [
{model: 'Golf', make: 'Volkswagen'},
{model: 'X1', make: 'BMW'},
{model: 'Clio', make: 'Renault'},
]

sortAlphabetically(cars, 'make');

I get the error: TS2345: Argument of type 'Person[]' is not assignable to parameter of type 'GenericObject[]'.   Type 'Person' is not assignable to type 'GenericObject'. Index signature is missing in type 'Person'. Same happens for Car[].

As a helper function, the key point is that it needs to adapt to any type of object that will be passed inside the array, without complaining about the type. Therefore I'm quite sure the problem is with the way I'm defining my GenericObject.

Can someone point out what I'm missing here? Thanks a lot.

2 Answers

First, you shouldn't be able to cast a Person to a GenericObject because Person holds number and string values when GenericObject only allows string values.

Second, there is an opened issue about the error you are experiencing

https://github.com/microsoft/TypeScript/issues/15300

You should replace the interface with a type


playground

type GenericObject = {
  [key: string]: string | number;
}

export const sortAlphabetically = (array: Array<GenericObject>, sortBy: keyof GenericObject) => {
  // Whatever
};

export type Person = {
  name: string;
  id: number;
}; 

const people: Person[] = [
{name: 'John', id: 1},
{name: 'Tony', id: 2},
{name: 'Mary', id: 3},
]

sortAlphabetically(people, 'name');

Well, the first thing to say a proper implementation will be troublesome.

What you seek :

  1. Pass an array
  2. Pass a key that belongs to a string property
  3. Sort the array by that key.

For those above you need to do a few things:

  • Narrow down the key parameter to allow for only string properties
    • GenericObject type not enough to provide this alone
    • You need some type-mapper that filters only string properties
  • Since there is no relation between GenericObject and Person types you can't do this casting: array.sort((a: GenericObject, b: GenericObject)
  • If you generalize that array type to any ish type then this won't work: a[sortBy].toLowerCase() because .toLowerCase() has to be applicable to any kind of object property. So you need a type guard that will narrow down that property into GenericObject

So TL;DR

interface GenericObject {
  [key: string]: string;
}
type FilterFlags<Base, Condition> = {
  [Key in keyof Base]:
  Base[Key] extends Condition ? Key : never
};
type AllowedNames<Base, Condition> =
  FilterFlags<Base, Condition>[keyof Base];
  
type SubType<Base, Condition> =
  Pick<Base, AllowedNames<Base, Condition>>;

export function sort<T>(array: Array<T>, sortBy: keyof SubType<T, string>) {
  let isKeyValid = false;

  // check the key exists in the object before attempting to sort by it
  if (array.length > 0) {
    isKeyValid = array.every(obj => Object.prototype.hasOwnProperty.call(obj, sortBy));
  }

  if (isKeyValid) {
    array.sort((a, b) =>
      (isGeneric(a, sortBy) && isGeneric(b, sortBy)) ? // narrow down type to GenericObject so that .toLowerCase becomes valid here
        a[sortBy].toLowerCase() < b[sortBy].toLowerCase()
          ? -1
          : a[sortBy].toLowerCase() > b[sortBy].toLowerCase()
            ? 1
            : 0 : 
            1 // this shouldn't be happening
    );
    return array;
  } else {
    return;
  }
};

const isGeneric = (a: any, key: keyof typeof a): a is GenericObject =>
  typeof a[key] === "string";

export interface Person {
  id: number;
  name: string;
}

const people: Person[] = [
  { name: 'John', id: 1 },
  { name: 'Tony', id: 2 },
  { name: 'Mary', id: 3 },
]

sort(people, "name"); // this won't allow passing "id" as parameter

Playground Link

SubType type mapper is taken from here

Related