Home > OS >  Stuck on typescript generic signature for GroupBy function
Stuck on typescript generic signature for GroupBy function

Time:07-29

Having some trouble after a few hours wrapping my head around what I'm doing wrong with this TS signature.

I'm writing a group by function:

const group = <T>(items: T[], fn: (item: T) => T[keyof T]) => {
  return items.reduce((prev, next) => {
    const prop = fn(next) as unknown as string;
    return {
      ...prev,
      [prop]: prev[prop] ? [...prev[prop], next] : [next],
    };
  }, {} as any);
};

group(data, (item) => item.type);

The return type gives me this:

const group: <Item>(items: Item[], fn: (item: Item) => string) => any

What I want is this:

const group: <Item>(items: Item[], fn: (item: Item) => string) => { admin: Item[], user: Item[] }

Here's the data structure:

interface User {
  type: string;
  name: string;
}

const data: User[] = [
  { type: 'admin', name: 'One' },
  { type: 'user', name: 'Two' },
  { type: 'admin', name: 'Three' },
];

I have tried something like this (with the object passed into reduce) but it gives me an error and I'm sure sure what the 'fix' is:

{} as { [key: T[keyof T]: T[]] }

Here's a TS Playground with the running code

Cheers!

CodePudding user response:

I notice that "admin" and "user" can only be obtained from the values of the data array (possible values of data[number]['type']).
TypeScript by default won't assume much from the values other than the values are strings. (The situation for 'type' and 'name' is different as they are obtained from keys)

But if you use as const for an array, TypeScript will infer more restrictions for the values as well.
Assuming the valid values of User['type'] is limited, it's possible to restrict it as shown below:

let PossibleUserTypes = ['admin', 'user'] as const;  

interface User {
  type: typeof PossibleUserTypes[number];
  name: string;
}

const data: User[] = [
  { type: 'admin', name: 'One' },
  { type: 'user', name: 'Two' },
  { type: 'admin', name: 'Three' },
];

const group = <T extends User, U extends string>(items: T[], fn: (item: T) => U) => {
  return items.reduce((prev, next) => {
    const prop = fn(next)
    return {
      ...prev,
      [prop]: prev[prop] ? [...prev[prop], next] : [next],
    };
  }, {} as {[x in U] : T[] } );
};
let temp = group(data, (item) => item.type);
console.log(temp);
/*
inferred typing: 
let temp: {
    admin: User[];
    user: User[];
}
*/

If you remove the 'as const' part, the output will just become just { [x: string]: User[];}, as it doesn't assume what the values of x (from the value for 'type') could be restricted to.

n.b. Instead of type: typeof PossibleUserTypes[number]; you can also use type: 'admin' | 'user' directly; the thing is, it's no longer just "any string" like in your original code.

Another alternative is using {} as Record<U,T[]> (here temp will be inferred to be Record<'user'|'admin', User[]> which still allows you to get code completion from temp. to temp.admin and temp.user), and again if you remove as const you lose the restriction (so it's not just .admin and .user anymore).

One may attempt using as const for the data array rather than the PossibleUserTypes, but then the resulting type will be very complex (you can check it out yourself):

const data  = [
  { type: 'admin', name: 'One' },
  { type: 'user', name: 'Two' },
  { type: 'admin', name: 'Three' },
] as const 
 

const group = <T, U extends string>(items: Readonly<T[]>, fn: (item: T) => U) => {
  return items.reduce((prev, next) => {
    const prop = fn(next)
    return {
      ...prev,
      [prop]: prev[prop] ? [...prev[prop], next] : [next],
    };
  }, {} as {[x in U] : T[] } );
};
let temp = group(data, (item) => item.type);
console.log(temp)

CodePudding user response:

In your function signature you do not specify the return type of the function. You just need to make the return of type T like this :

const group = <T>(items: T[], fn: (item: T) => T[keyof T]):T => { return items.reduce((prev, next) => {
const prop = fn(next) as unknown as string;
return {
  ...prev,
  [prop]: prev[prop] ? [...prev[prop], next] : [next],
};}, {} as any);};
console.log(group<Item>(data, (item) => item.type));

See this Typescript playground TS Playground

Referring to your comment I have changed a little bit the code. check here A returned type must be a static type.

  • Related