Reputation: 733
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
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);
}
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
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
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!
Upvotes: 3