Mirco Bellagamba
Mirco Bellagamba

Reputation: 1218

TypeScript: How to cast generic type to another generic type when overloading a function?

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

Answers (2)

apokryfos
apokryfos

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.

Playground link

Upvotes: 0

bugs
bugs

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

Related Questions