TypeScript:returns 与参数类型相同的通用函数

TypeScript: Generic function which returns the same type as parameter

考虑这个(不编译):

function roundTo<T = number | null | undefined>(
  num: T,
  decimals: number,
): T {
  if (num === null || num === undefined) return num;

  const factor = Math.pow(10, decimals);
  return (Math.round(num * factor) / factor);
}

我想要 return 传递给 roundTo() 函数的相同类型。

例如:

const num1: number | null = 1.123456;
roundTo(num1, 1) // return type number | null

const num2: number = 1.123456;
roundTo(num2, 1) // return type number

const num3 = null;
roundTo(num3, 1) // return type null

roundTo 的 return 类型在编译时是已知的,因此希望能够根据第一个参数中传递的类型从那里继承类型。

我可以通过转换 return 类型 as any 来进行编译,但这会破坏类型安全。我也可以通过使用 extends 而不是 = 并使用 return 类型 as T 进行编译,但它具有 returning [=] 的不良行为19=] 当传入 nullundefined 时。

如何让 TypeScript 表现出所需的行为?

相关:

应该是T extends ...,不是T = ...。后一种形式是“T 的默认值”,它不是推断的,而是总是按照声明的那样来使用,基本上扼杀了整个想法。

  function roundTo<T extends number | null | undefined>(num: T, decimals: number): T {
    if (num == null) return num;
    const factor = Math.pow(10, decimals);
    return (Math.round((num as number) * factor) / factor) as T;
  }

  function test(x: number, y: null, z: undefined, t?: number, u?: null) {
    let x1 = roundTo(x, 2); // -> number
    let x2 = roundTo(y, 2); // -> null
    let x3 = roundTo(z, 2); // -> undefined
    let x4 = roundTo(t, 2); // -> number | undefined
    let x5 = roundTo(u, 2); // -> null | undefined
  }

实际上,您可以摆脱一种类型的强制转换 num as number 但可能无法摆脱另一种类型(至少不是一种简单的方法)

  function roundTo<T extends number | null | undefined>(num: T, decimals: number): T {
    if (typeof num === 'number') {
      // here typescript knows, that num is specific subtype of T - number
      // but doesn't extend this to the T itself
      const factor = Math.pow(10, decimals);
      return Math.round(num * factor) / factor as T;
    }
    return num;
  }

如果您真的想要强制打字稿推断函数内部 T 的实际类型,条件类型可能是一种方式,但恕我直言,这太过分了。

来自@Johnny Oshika 的另一点 - 为避免不必要的数字类型缩小,可以使用重载:

  function roundTo(num: number, decimals: number): number;
  function roundTo<T extends number | null | undefined>(num: T, decimals: number): T;
  function roundTo<T extends number | null | undefined>(num: T, decimals: number): T {
   // function body
  }

此答案归功于@jcalz。

使用重载签名设置 T extends number ? number : T 的条件 return 类型。然后实现可以放宽类型推断。

// Overload signature
export function roundTo<T extends number | null | undefined>(
  num: T,
  decimals: number,
): T extends number ? number : T;

// Implementation
export function roundTo(
  num: number | null | undefined,
  decimals: number,
) {
  if (num === null || num === undefined) return num;
  const factor = Math.pow(10, decimals);
  return Math.round(num * factor) / factor;
}

示例:

roundTo(undefined, 2) // Returns type `undefined`
roundTo(null, 2) // Returns type `null`
roundTo(1.234 as number | undefined, 2) // Returns type `number | undefined`
roundTo(1.234 as number | null, 2) // Returns type `number | null`
roundTo(1.234, 2) // Returns type `number`