Search code examples
typescriptgenericstypescasting

TypeScript: Generic function which returns the same type as parameter


Consider this (which doesn't compile):

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

I would like to return the same type that's passed in to the roundTo() function.

For example:

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

The return type of roundTo is known at compile time, so the desire is to be able to carry the type forward from there based on the type passed in the first parameter.

I can make this compile by casting the return type as any, but that would break type safety. I can also make this compile by using extends instead of = and casing the return type as T, but it has the undesired behavior of returning any when null or undefined is passed in.

How can I get TypeScript to exhibit the desired behavior?

Related: https://stackoverflow.com/a/51195834/188740 https://stackoverflow.com/a/57529925/188740


Solution

  • Credit for this answer goes to @jcalz.

    Use an overload signature to set a conditional return type of T extends number ? number : T. Then the implementation can relax the type inference.

    // 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;
    }
    

    Examples:

    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`