Defining the type NumMax

78 Views Asked by At

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!

2

There are 2 best solutions below

0
Dimava On BEST ANSWER

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
6
captain-yossarian from Ukraine On

Yes, it is possible:

type ComputeRange<
  N extends number,
  Result extends Array<unknown> = [],
> =
  (Result['length'] extends N
    ? Result
    : ComputeRange<N, [...Result, Result['length']]>
  )

type IsLiteralNumber<N> = N extends number ? number extends N ? false : true : false

type Last<T extends any[]> = T extends [...infer _, infer Last] ? Last extends number ? Last : never : never

type NumMax<A extends number, B extends number> =
  IsLiteralNumber<[...ComputeRange<B>][Last<[...ComputeRange<A>]>]> extends true ? B :A

type Result = NumMax<10, 15> // 15

If you want also handle negative numbers you can check this issue