I am trying to add types to function that takes array of objects and groups them by key.
Here is my code:
interface ITask {
assignedTo: string;
tasks: Array<string>,
date: string;
userId: number;
};
interface IFormattedOutput<T> {
[prop: string]: Array<T>;
}
const testData: Array<ITask> = [
{
assignedTo: 'John Doe',
tasks:['Code refactoring', 'Demo', 'daily'],
date: '1-12-2022',
userId: 123
},
{
assignedTo: 'Maximillian Shwartz',
tasks:['Onboarding','daily'],
date: '1-12-2022',
userId: 222
},
{
assignedTo: 'John Doe',
tasks:['Team Menthoring', 'daily', 'technical call'],
date: '1-13-2022',
userId: 123
},
{
assignedTo: 'John Doe',
tasks:['New dev onboardin','daily'],
date: '1-12-2022',
userId: 123
}
]
const groupByKey = <T, K extends keyof T>(list: Array<T>, key: K):IFormattedOutput<T> => {
return list.reduce((reducer, x) => {
(reducer[x[key]] = reducer[x[key]] || []).push(x);
return reducer;
}, {});
};
const res = groupByKey <ITask, 'assignedTo'>(testData, 'assignedTo');
console.log(res);
Unfortunately o am getting TS error 'Type 'T[K]' cannot be used to index type '{}'.'
Is there anything to do to fix this error?
CodePudding user response:
2 problems:
not typing the reducer seed properly, causing your current error
even if you fix 1, you'd get a similar error, because you haven't restricted
K
to only point at string values of T, so the compiler will correctly warn you that it doesn't know if typeT[K]
can indexIFormattedOutput<T>
ideal solution here will have the compiler throwing errors at you when you try to use this function improperly, so you need to define a type that only allows keys where T[K]
is of type string, can do so like this:
// this maps type T to only the keys where the key has a string value
type StringKeys<T> = {
[k in keyof T]: T[k] extends string ? k : never
}[keyof T];
// this narrows type T to only it's properties that have string values
type OnlyString<T> = { [k in StringKeys<T>]: string };
then type your function like so:
// T extends OnlyString<T> tells compiler that T can be indexed by StringKeys<T>
// then you type key to be StringKeys<T> so compiler knows it must have a string value
// now the key must be a key of T that has a string value, so the index issues are solved
const groupByKey = <T extends OnlyString<T>>(list: T[], key: StringKeys<T>): IFormattedOutput<T> => {
return list.reduce((reducer, x) => {
(reducer[x[key]] = reducer[x[key]] || []).push(x);
return reducer;
}, {} as IFormattedOutput<T>); // also need to type reducer seed correctly
};
now the wrong input will throw type errors at you.
so these work as expected:
// note you don't need to declare the types, TS can infer them
groupByKey(testData, 'assignedTo');
groupByKey(testData, 'date');
but if you did:
groupByKey(testData, 'tasks');
groupByKey(testData, 'userId');
you'd get compiler errors
CodePudding user response:
There are 2 steps, I tested on my local and succeeded.
First, I changed your interface IFormattedOutput<T>
to this:
interface IFormattedOutput<T> {
[key: string]: Array<T>;
}
The groupByKey
function will be like as follow:
// type guard for T[K] to be string otherwise throw an error
const isString = (propKey: any): propKey is string => typeof propKey === 'string';
const groupByKey = <T, K extends keyof T>(list: Array<T>, key: K): IFormattedOutput<T> =>
list.reduce((reducer, x) => {
const propKey = x[key];
// this type narrowing is necessary to get rid of the
// "type T[K] cannot be used to index type IFormattedOutput
// due to type ambiguity.
if (isString(propKey)) {
(reducer[propKey] = reducer[propKey] || []).push(x);
} else {
throw new Error(`Expected string, got '${typeof propKey}'.`);
}
return reducer;
}, {} as IFormattedOutput<T>);
I think Type guard and Typescript type narrowing are really important practices and seem to solve a lot of TS issues that I have come across.
Please note that even when this Typescript issue is solved, my eslint still has a warning because you are trying to reassign the reducer, so I silenced it with // eslint-disable-next-line no-param-reassign
.
Let me know if this solves your issue.