Timothy Jones
Timothy Jones

Reputation: 22125

Flow types throwing error on seemingly unrelated types

I have the following flow code (available here):

type SquareState = {
  value: number,
};

type InternalSquare = {
  value: number | null,
};

export function getMax(squares: Array<InternalSquare> | Array<SquareState>): number {
   return squares.reduce(function(a : number, b : InternalSquare | SquareState): number {
    if (b.value === null) {
      return a;
    } else {
      return Math.max(a, b.value);
    }
  }, 0);
}

Which is generating this error:

 9: export function getMax(squares: Array<InternalSquare> | Array<SquareState>): number {
                                                             ^ number [1] is incompatible with null [2] in property `value`.
   References:
   2:   value: number,
               ^ [1]
   6:   value: number | null,
                        ^ [2]

As far as I understand, there should be no error, because there's a guard against null. The code works fine with either:

export function getMax(squares: Array<SquareState>):  number

or

export function getMax(squares: Array<InternalSquare>): number

But not with Array<SquareState> | Array<InternalSquare>

Also, if I replace the reduce with a for loop, it works:

export function getMax(squares: Array<InternalSquare> | Array<SquareState>): number {
  var max = 0;
  for(var i = 0 ; i < squares.length ; i ++) {
     if (squares[i].value === null) {
       continue;
     } else {
       max = Math.max(max, squares[i].value);
     }
  }
  return max;
}

What am I missing?

Upvotes: 3

Views: 69

Answers (1)

James Kraus
James Kraus

Reputation: 3478

I think you might want to accept an array of either InternalSquare or SquareState:

(Try)

type SquareState = {
  value: number,
};

type InternalSquare = {
  value: number | null,
};

export function getMax(squares: Array<InternalSquare | SquareState>): number {
    return squares.reduce(function(a : number, b : InternalSquare | SquareState): number {
      if (b.value === null) {
        return a;
      } else {
        return Math.max(a, b.value);
      }
    }, 0);
}

It doesn't seem like flow realizes that reducing the union of two arrays (Array<A> | Array<B>) is equivalent to reducing an array of the union of both types (Array<A | B>). This might even be PR-worthy.

Interestingly, spreading the array allows flow to understand the type as Array<InternalSquare | SquareState>:

(Try)

type SquareState = {
  value: number,
};

type InternalSquare = {
  value: number | null,
};

export function getMax(squares: Array<InternalSquare> | Array<SquareState>): number {
    const afterSpread = [...squares]
    return afterSpread.reduce(function(a : number, b : InternalSquare | SquareState): number {
      if (b.value === null) {
        return a;
      } else {
        return Math.max(a, b.value);
      }
    }, 0);
}

Even more interesting, if you change one of the arrays (but not both!) to a $ReadOnlyArray, it also understands what's going on here:

type SquareState = {
  value: number,
};

type InternalSquare = {
  value: number | null,
};

export function getMax(squares: $ReadOnlyArray<InternalSquare> | Array<SquareState>): number {
    return squares.reduce(function(a : number, b : InternalSquare | SquareState): number {
      if (b.value === null) {
        return a;
      } else {
        return Math.max(a, b.value);
      }
    }, 0);
}

I'd hazard a guess that it actually evaluates each of the possibilities in that case and finds both possibilities work. Total speculation though.

Upvotes: 2

Related Questions