TypeScript:重载函数时如何将泛型类型转换为另一个泛型类型?

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

我想定义一个从数组创建字典的通用函数。该函数将 arraykeySelector 和可选的 valueSelector 作为参数。如果未提供 valueSelector,则函数回退到身份函数。我希望 Typescript 能够理解类型 VT 相同。相反,编译器给我错误 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>
  );
}

这里是 link 到 TypeScript Playground

我找到的唯一解决方案是使用 any 关键字。

  valueSelector: (item: T) => V = (item: any) => item

期望的结果如下:

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);

有没有更好的方法来定义函数的类型?

你可以把valueSelector参数的return类型从简单的V放宽到V | T,这正是你想表达的,同时时间为您的泛型 V 提供默认类型(如果您不将第三个参数传递给该函数,它将被推断为 unknown)。

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>

我觉得你可以考虑用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>
  );
}

虽然这不能直接解决问题,但它确实提供了关于函数签名的明确反馈。无论使用哪个签名来调用它,该实现只应涵盖函数的作用。

Playground link