Home > Software engineering >  TypeScript: type "XXX" is not assignable to type "XXX|YYY"
TypeScript: type "XXX" is not assignable to type "XXX|YYY"

Time:11-18

I'm writing a higher-order function, opposite(fn). This opposite takes a boolean or numeric fn function as input, and returns a new function, which should be of the same type as fn. If the original function is of boolean-returning type, the new function returns "not that boolean". If the original function is of number-returning type, the new function returns 1 or 0 depending if the result was zero or not. You can use opposite to invert a filter, from someArray.filter(testCondition) to someArray.filter(opposite(testCondition)).

The problem: both return sentences produce an error Type 'number' is not assignable to type 'ReturnType<T>' ts(2322).

Why cannot I assign a boolean or number to a boolean|number? How should I apply data types to my opposite function?

const opposite =

  < T extends
      | ((...args: any[]) => boolean)
      | ((...args: any[]) => number)
  >(fn: T) =>

  (...args: Parameters<T>): ReturnType<T> => {

    const result = fn(...args);
    if (typeof result === "number") {
      return result === 0 ? 1 : 0;
    } else {
      return !result;
    }
  };

CodePudding user response:

I trip up on things like this every time I pick up TS again.

I don't think you can type this without partially turning off type checking one way or another.

Note that I have added the util Widen because your function signature was wrong: if fn is () => true as const, then opposite(fn) is () => true instead of () => boolean

type Widen<T> = T extends number ? number : T extends boolean ? boolean : T;

const opposite =

  < T extends
      | ((...args: any[]) => boolean)
      | ((...args: any[]) => number)
  >(fn: T) =>

  (...args: Parameters<T>): Widen<ReturnType<T>> => {

    const result = fn(...args);
    if (typeof result === "number") {
      return result === 0 ? 1 : 0 as any;
    //                            ------
    } else {
      return !result as any;
    //               ------
    }
  };
const opposite:{
    <Args extends unknown[]>(f: (...args: Args) => boolean): (...args: Args) => boolean
    <Args extends unknown[]>(f: (...args: Args) => number): (...args: Args) => 0 | 1
} = <Args extends unknown[]>(f: (...args: Args) => boolean | number) => (...args: Args): any => {
//                                                                                       ---
    const result = f(...args);

    if(typeof result === 'number') {
        return result === 0 ? 1 : 0;
    } else {
        return !result;
    }
};
type Widen<T> = T extends number ? number : T extends boolean ? boolean : T;

const opposite =
    <Args extends unknown[], R extends boolean | number>
        (f: (...args: Args) => R) => (...args: Args): Widen<R> => {
            const result = f(...args);

            if(typeof result === 'number') {
                return result === 0 ? 1 : 0 as any;
                //                          ------ 
            } else {
                return !result as any;
                //             ------
            }
        };

A few things can conspire for it not to compile:

  • In your case I think the culprit is that ReturnType<T> is undetermined: it is not number | boolean and the compiler won't compute the value.
  • In the case of the implementation with overloads, the problem is that overloads are an intersection, and the intersection boolean & number is never.
  • The problem with the last implementation is that R could be provided by the caller: If they pass in 5, boolean no longer extends R.

I may be slightly wrong about those interpretations but this mental model works quite well and the bottom line is you are stuck.

  • Related