typescripttypescript-genericsgeneric-constraints

Defining the type NumMax


Is it possible to define a type NumMax<x, y> in Typescript that takes the maximum of two types, such that NumMax<z, z> is assignable to z? The following definition does not work:

export type NumMax<x extends number, y extends number> = 
  [x, y] extends [0, 0] ? 0
: [x, y] extends [0, 1] ? 1
: [x, y] extends [0, 2] ? 2
: [x, y] extends [0, 3] ? 3
: [x, y] extends [1, 0] ? 1
: [x, y] extends [1, 1] ? 1
: [x, y] extends [1, 2] ? 2
: [x, y] extends [1, 3] ? 3
: [x, y] extends [2, 0] ? 2
: [x, y] extends [2, 1] ? 2
: [x, y] extends [2, 2] ? 2
: [x, y] extends [2, 3] ? 3
: [x, y] extends [3, 0] ? 3
: [x, y] extends [3, 1] ? 3
: [x, y] extends [3, 2] ? 3
: [x, y] extends [3, 3] ? 3
: x;

function func1<x extends number, y extends number>(xx: x, yy: y): NumMax<x, y> {
  return null as any; // ... unimportant code ...
}
function func2<z extends number>(z1: z, z2: z): z {
  return func1(z1, z2); // ERROR: Type 'NumMax<z, z>' is not assignable to type 'z'
}

Thank you!


Solution

  • Playground: https://tsplay.dev/WKvjpm

    Making the Max type is relatively simple if you know what to do:

    /** make array of N zeroes */
    /** works for integer 0 <= N < 1000 */
    type ArrayOfLength<N extends number, P extends 0[] = []> = 
    | P['length'] extends N ? P
    : ArrayOfLength<N, [...P, 0]>
    
    /** return A if array of A zeroes is longer then array of B zeroes */
    /** works for integers 0 <= A,B < 1000 */
    /** `number` is considered to be `0`, use `number extends A` checks if needed */
    type Max<A extends number, B extends number> = 
    | ArrayOfLength<A> extends [...ArrayOfLength<B>, ...0[]] ? A : B
    
    type m1 = Max<4, 5>
    //   ^? type m1 = 5
    
    function func1<x extends number, y extends number>(xx: x, yy: y): Max<x, y> {
      return Math.max(xx, yy) as Max<x, y>
    }
    

    Hovewer, if you write your second function as is, you'll rul into a bug:

    function func2_bad<z extends number>(z1: z, z2: z): z {
      return func1(z1, z2)
    }
    let bad = func2_bad(2, 4)
    //  ^? let bad: 2 | 4
    // function func2_bad<2 | 4>(z1: 2 | 4, z2: 2 | 4): 2 | 4
    

    To avoid it, disable inferense of one of arguments

    type NoInfer<T extends number> = T extends infer V extends number ? V : T;
    
    function func2<z extends number>(z1: z, z2: NoInfer<z>): z {
      return func1(z1, z2 as z)
    }
    func2(2, 4)
    //       ^! Argument of type '4' is not assignable to parameter of type '2'.(2345)
    
    

    edit: here's a variant that works with number unions (but its laggy):

    /** return maximum of number union, or `number` if some numbers are `number`, non-integer, or negative */
    type Max<NN extends number, _a extends 0[] = []> =
       | number extends NN ? number
       : _a['length'] extends (999 | NN) ? (
          _a['length'] extends 999 ? number
          : [Exclude<NN, _a['length']>] extends [never] ? (
             _a['length']
          ) : Max<Exclude<NN, _a['length']>, [..._a, 0]>
       ) : Max<NN, [..._a, 0]>
    
    function max<X extends number, Y extends number>(x: X, y: Y): Max<X | Y> {
       return Math.max(x, y) as never
    }
    
    function max1<Z extends number>(z: Z & Max<Z>, z2: Z & Max<Z>): Max<Z> {
       return max(z, z2)
    }
    
    max1(2, 4) // err
    max1(7, 9) // err