Search code examples
typescriptobjectgenericshelperany

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


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.


Solution

  • 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