Reputation: 1218
I want to define a generic function that creates a dictionary from an array. The function takes as parameters the array
, a keySelector
and an optional valueSelector
. If no valueSelector
is provided, the function fallbacks to the identity function. I wished Typescript would understand that type V
is the same as T
. Instead, the compiler gives me the error Type 'T' is not assignable to type 'V'
.
export function arrayToDictionary<T, K extends string | number | symbol, V>(
array: T[],
keySelector: (item: T) => K,
valueSelector: (item: T) => V = (item) => item // ERROR Type 'T' is not assignable to type 'V'
): Record<K, V> {
return array.reduce(
(acc, curr) => ({ ...acc, [keySelector(curr)]: valueSelector(curr) }),
{} as Record<K, V>
);
}
Here is the link to the TypeScript Playground.
The only solution I found is to use the any
keyword.
valueSelector: (item: T) => V = (item: any) => item
The desired result is the following:
const array = [
{ id: 1, name: "John" },
{ id: 2, name: "Will" },
{ id: 3, name: "Jane" },
];
// dict1 type should be Record<number, {id: number; name: string; }>
const dict1 = arrayToDictionary(array, p => p.id);
// dict2 type should be Record<number, string>
const dict2 = arrayToDictionary(array, p => p.id, p => p.name);
Is there a better way to define the type of the function?
Upvotes: 0
Views: 994
Reputation: 40730
I think you can consider using function overloading:
function arrayToDictionary<T, K extends string | number | symbol>(
array: T[],
keySelector: (item: T) => K
) : Record<K, K>;
function arrayToDictionary<T, K extends string | number | symbol, V>(
array: T[],
keySelector: (item: T) => K,
valueSelector: (item: T) => V
): Record<K, V>;
function arrayToDictionary<T, K extends string | number | symbol, V>(
array: T[],
keySelector: (item: T) => K,
valueSelector?: (item: T) => V
): Record<K, V> {
return array.reduce(
(acc, curr) => ({ ...acc, [keySelector(curr)]: valueSelector?.(curr)??curr }),
{} as Record<K, V>
);
}
While this does not directly resolve the issue it does provide clear feedback on the function signatures. The implementation is only expected to cover what the function does regardless of which signature was used to call it.
Upvotes: 0
Reputation: 15323
You can relax the return type of the valueSelector
param from simply V
to V | T
, which is exactly what you want to express, and at the same time provide a default type for your generic V
(which would otherwise be inferred as unknown
if you don't pass a third argument to the function).
function arrayToDictionary<T, K extends string | number | symbol, V = T>(
array: T[],
keySelector: (item: T) => K,
valueSelector: (item: T) => V | T = item => item
): Record<K, V> {
return array.reduce(
(acc, curr) => ({ ...acc, [keySelector(curr)]: valueSelector(curr) }),
{} as Record<K, V>
);
}
const arr = [1,2,3,4]
const dict1 = arrayToDictionary(arr, n => n.toString() + '!', n => n > 2) //Record<string, boolean>
const dict2 = arrayToDictionary(arr, n => n.toString() + '!') //Record<string, number>
Upvotes: 1