Search code examples
typescripttypescript-generics

Typescript type generic constraint implements the `+` operator


I have this sumBy function that looks like this:

export function sumBy<T>(array: T[], mapperFn: (item: T) => number): number {
  return array.reduce((sum, currentItem) => sum + mapperFn(currentItem), 0);

and I wanted to improve it to accept also bigint type.

But of course, I have to tell the + operator, as well as the initialiser their proper type. For the latter, I have a clean solution (if (sum === undefined) return currentValue;), but for the former, I can't find a proper way. My best guess for now is this:

export function sumBy<T, U extends number | bigint>(array: T[], mapperFn: (item: T) => U): U | undefined {
  return array.reduce((sum: U | undefined, currentItem: T) => {
    const currentValue = mapperFn(currentItem);
    if (sum === undefined) return currentValue;
    if (typeof currentValue === "number" && typeof sum === "number") return (sum + currentValue) as U;
    if (typeof currentValue === "bigint" && typeof sum === "bigint") return (sum + currentValue) as U;
  }, undefined);
}

But this is so heavy, and it doesn't even return the expected type for empty array ...

How come I can't:

  • tell typescript that both types implement the + operator
  • and/or tell typescript that the input type is the same as the output type, which means a signature like:
sumBy<T, U is number or bigint>(array: T[], mapperFn: (item: T) => U): U

Thanks!


Solution

  • I would use function overloads here to match the input and output:

    Playground

    const ensureBigint = (val: number | bigint): bigint => typeof val === 'number' ?  BigInt(val) : val;
    function sumBy(array: [], mapperFn: (item: any) => any): undefined;
    function sumBy<T>(array: [T, ...T[]], mapperFn: (item: T) => number): number;
    function sumBy<T>(array: T[], mapperFn: (item: T) => number): number | undefined;
    function sumBy<T>(array: [T, ...T[]], mapperFn: (item: T) => number | bigint): bigint;
    function sumBy<T>(array: T[], mapperFn: (item: T) => number | bigint): bigint | undefined;
    function sumBy<T>(array: T[], mapperFn: (item: T) => number | bigint): number | bigint | undefined {
      if(!array.length) return undefined;
      let sum: bigint | number = 0;
      for(const item of array){
        const val = mapperFn(item);
        if (typeof val === "number" && typeof sum === 'number'){
          sum = sum + val;
          continue;
        }
        sum = ensureBigint(sum) + ensureBigint(val);
      }
      return sum;
    }
    
    
    const s = sumBy([{num:0n}, {num:1}], item => item.num); // bigint
    console.log(s);
    
    const s2 = sumBy([1, 1], item => item); // number
    console.log(s2);
    
    const s3 = sumBy([], item => item); // undefined;
    console.log(s3);
    
    const arr = [1, 2n];
    arr.length = 0;
    const s4 = sumBy(arr, item => item); // bigint | undefined