(I'm using strict null checks)

I have the following arrow function, with overloaded type:

    type INumberConverter = {
      (value: number): number;
      (value: null): null;
    };
    const decimalToPercent: INumberConverter = (value: number | null): number | null => {
      if (!value) {
        return null;
      }
      return value * 100;
    };

To my understanding from other questions (Can I use TypeScript overloads when using fat arrow syntax for class methods?) this should be a valid. Never the less I get the following error:

TS2322: Type '(value: number | null) => number | null' is not assignable to type 'INumberConverter'.   Type 'number | null' is not assignable to type 'number'.     Type 'null' is not assignable to type 'number'

If I write this function regularly (with the function keyword):

    function decimalToPercent(value: null): null;
    function decimalToPercent(value: number): number;
    function decimalToPercent(value: number | null): number | null {
      if (!value) {
        return null;
      }
      return value * 100;
    }

It works without errors.

I need to use arrow function so this will not be change, and I need this overloading so typescript knows decimalToPercent(1) cannot be null.

Why doesn't it work the way I did it, and how can I fix it?

1 Answers

1
Titian Cernicova-Dragomir On Best Solutions

Rules for compatibility between overload signatures and implementation signature are much more relaxed than they are for assignment.

In this case you are trying to assign a function that may return null to a function that has an overload that forbids the returning of null ((value: number): number;). The compiler will rightly find this troubling. For overloads, since the signatures and the implementation are all written as one unit the compiler assumes 'You know what you are doing' (rightly or not).

You can work around it in several ways:

You can use a type assertion, although you will loose most type checking for implementation, signature compatibility:

type INumberConverter = {
  (value: number): number;
  (value: null): null;
};
const decimalToPercent = ((value: number | null): number | null => {
  if (!value) {
    return null;
  }
  return value * 100;
}) as INumberConverter;

You could also use a regular function and capture this as in the old ES5 days, although this solution means duplicating a lot of the function signature:

type INumberConverter = {
  (value: number): number;
  (value: null): null;
};

class X {
    decimalToPercent: INumberConverter;
    multiper = 100;
    constructor() {
        let self = this;
        function decimalToPercent(value: number): number;
        function decimalToPercent(value: null): null;
        function decimalToPercent(value: number | null): number | null {
            if (!value) {
                return null;
            }
            // use self
            return value * self.multiper;
        };
        this.decimalToPercent = decimalToPercent;
    }
}

Or possibly the simplest solution is to use bind in the constructor and write the function as a regular method:

class X {

    decimalToPercent(value: number): number;
    decimalToPercent(value: null): null;
    decimalToPercent(value: number | null): number | null {
        if (!value) {
            return null;
        }
        return value * this.multiper;
    };
    multiper = 100;
    constructor() {
        this.decimalToPercent = this.decimalToPercent.bind(this);
    }
}