I'm trying to implement clamp for multiple number-ish types simultaneously like so:
import BigNumber from 'bignumber.js'
export const clamp = <T extends number | BigNumber>(min: typeof n, n: T, max: typeof n): T => {
if (isNumber(n)) {
return Math.max(min, Math.min(n, max)) as T
}
if (isBigNumber(n)) {
return BigNumber.max(min, BigNumber.min(n, max)) as T
}
}
const isNumber = (n: number | BigNumber): n is number => {
return typeof n === 'number'
}
const isBigNumber = (n: number | BigNumber): n is BigNumber => {
return n instanceof BigNumber
}
But code failes to compile with the following error:
TypeScript error in clamp.ts(5,21):
Argument of type 'number | BigNumber' is not assignable to parameter of type 'number'.
Type 'BigNumber' is not assignable to type 'number'. TS2345
3 | export const clamp = <T extends number | BigNumber>(min: typeof n, n: T, max: typeof n): T => {
4 | if (isNumber(n)) {
> 5 | return Math.max(min, Math.min(n, max)) as T
| ^
6 | }
7 |
8 | if (isBigNumber(n)) {
Shouldn't types of min
and max
be inferred as number
on line 5? If not, how can one assure Typescript of correctness of types?
CodePudding user response:
Unfortunately this is one of the cases where Typescript's type system comes short. There is a proposal to allow intersection type guards, this would enable us to have a type guard such as:
const isNumber = (n: number | BigNumber, min: typeof n, max: typeof n): n is number & min is number & max is number => {
return typeof n === 'number';
}
Until such proposal is implemented, you'll have to explicitly type-check both min
and max
variables:
import BigNumber from 'bignumber.js'
export const clamp = <T extends number | BigNumber>(min: T, n: T, max: T): number | BigNumber => {
if (isNumber(n) && isNumber(min) && isNumber(max)) {
return Math.max(min, Math.min(n, max));
}
if (isBigNumber(n) && isBigNumber(min) && isBigNumber(max)) {
return BigNumber.min(min, BigNumber.max(n, max));
}
throw new TypeError("Every parameter of this function must be either of type number or an instance of BigNumber");
}
const isNumber = (n: number | BigNumber): n is number => {
return typeof n === 'number'
}
const isBigNumber = (n: number | BigNumber): n is BigNumber => {
return n instanceof BigNumber
}
or cast both min and max:
import BigNumber from 'bignumber.js'
export const clamp = <T extends number | BigNumber>(min: T, n: T, max: T): number | BigNumber => {
if (isNumber(n)) {
return Math.max(min as number, Math.min(n, max as number));
}
return BigNumber.min(min as BigNumber, BigNumber.max(n, max as BigNumber));
}
const isNumber = (n: number | BigNumber): n is number => {
return typeof n === 'number'
}
CodePudding user response:
Your type narrowing wasn't covering every parameters of your function !
class BigNumber {
static min(a: BigNumber, b: BigNumber): BigNumber { return new BigNumber() };
static max(a: BigNumber, b: BigNumber): BigNumber { return new BigNumber() };
};
export const clamp = <T extends number | BigNumber>(min: T, n: T, max: T): T => {
if (typeof n === 'number' && typeof min === 'number' && typeof max === 'number') {
return Math.max(min, Math.min(n, max)) as T
}
if (n instanceof BigNumber && min instanceof BigNumber && max instanceof BigNumber) {
return BigNumber.min(min, BigNumber.max(n, max)) as T
}
}
CodePudding user response:
It may be, or it may be not - you can still pass an argument of number | BigNumber
to the function:
let x: number | BigNumber = 123;
clamp(x, x, x); // <- you see
Use either:
- function overloading
function clamp(n: number, min: number, max: number): number; // <- overload 1
function clamp(n: BigNumber, min: BigNumber, max: BigNumber): BigNumber; // <- overload 2
function clamp(n: number|BigNumber, min: number|BigNumber, max: number|BigNumber): number|BigNumber{
return n; // example impl
}
- default type:
function clamp<T extends number | BigNumber = number // <- default type
>(n: T, min: T, max: T): T {
return n;
}