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!
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