exk0730
exk0730

Reputation: 733

Complicated generic typing for Typescript

I have been banging my head against a wall trying to understand how to make a function type-safe, but have not been able to do it. The function should take in an object and return a number. Here's a very simple example (for my actual application, the interfaces are more complex).

interface Parent {
  id: number;
  children: Child[];
}

interface Child {
  text: string;
}

const parents: Parent[] = [
  {
    id: 1, 
    children: [
      {text: 'Child 1a'}, {text: 'Child 1b'}, 
      {text: 'Child 1c'}, {text: 'Child 1d'}, 
      {text: 'Child 1e'}
    ]
  },
  {
    id: 2, 
    children: [
      {text: 'Child 2a'}, {text: 'Child 2b'}, {text: 'Child 2c'}
    ]
  }  
];

function getMaxNumChildren<T>(data: T[], childKey: keyof T) {
  return data.reduce((max: number, parent: T) => {
    return max > parent[childKey].length ? max : parent[childKey].length;
  }, 0);
}

console.log(getMaxNumChildren<Parent>(parents, 'children')); // 5

So, as you can imagine, parent[childKey].length throws an error because typescript doesn't actually know that T[keyof T] is an array.

I've tried casting to any[], among other random things, but I can't seem to get this right and keep the function purely generic. Any ideas?

Upvotes: 0

Views: 375

Answers (3)

smac89
smac89

Reputation: 43234

You are over-complicating it. Just use a Parent array

function getMaxNumChildren<T>(data: Parent[]T[], keyGetter: (obj: T) => Array<unknown>) {
  return data.reduce((max: number, parent: ParentT) => {
    return Math.max > (keyGetter(parent.children).length ?, max : parent.children.length;);
  }, 0);
}


Updated answer

Better to use a callback than abusing the type system.

function getMaxNumChildren<T>(data: T[], keyGetter: (obj: T) => Array<unknown>) {
  return data.reduce((max: number, parent: T) => {
    return Math.max(keyGetter(parent).length, max);
  }, 0);
}

You use it like this:

console.log(getMaxNumChildren<Parent>(parents, (p) => p.children));

Upvotes: 0

casieber
casieber

Reputation: 7542

You need to let TypeScript know about two generic types, not just one. The first being some key and the second being some object where that key has a value of an array.

Try something like this:

function getMaxNumChildren<TKey extends string, TObj extends { [key in TKey]: unknown[] }>(data: TObj[], childKey: TKey) {
    // ...
}

Upvotes: 0

jcalz
jcalz

Reputation: 330316

The simplest way I can imagine getting this to work is to make the function generic in K, the type of childKey, and annotating that data is an array of objects with keys in K and properties with a numeric length property, like this:

function getMaxNumChildren<K extends keyof any>(
  data: Array<Record<K, { length: number }>>,
  childKey: K
) {
  return data.reduce((max, parent) => {
    return max > parent[childKey].length ? max : parent[childKey].length;
  }, 0);
}

The compiler is then able to verify that parent[childkey] has a numeric length and there are no errors. Then you call it like this:

console.log(getMaxNumChildren(parents, 'children')); // 5

Note that you don't call getMaxNumChildren<Parent>(...) anymore because the generic type is the key type, not the object type. You could call getMaxNumChildren<"children">(...) if you want, but I'd just let type inference work for you here.


Hope that works for you. If it doesn't work for your use case, please consider editing the question to include more details. Good luck!

Link to code

Upvotes: 3

Related Questions