Home > other >  Typescript - Failed to infer argument type
Typescript - Failed to infer argument type

Time:03-14

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;
}
  • Related