Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flow type refinement to non-nullable doesn't work

Somewhere in my Flow annotated JavaScript code I have an object obj defined with nullable field field.

let obj: { field: ?number } = { field: 1 };

I have a function func that accepts argument arg which has the same structure as obj but field should be non-nullable at this time.

function func(arg: {field: number}){
    // do something
}

I'm calling the func with obj after checking that the obj.field is not null or undefined, hoping that Flow will do it's type refining magic and understand that obj.field is always number and it's type is OK to pass to the func.

if(obj.field !== null && obj.field !== undefined){
    func(obj);
}

Surprisingly getting an error, Flow doesn't like it:

14:   func(obj);
           ^ Cannot call `func` with `obj` bound to `arg` because null or undefined [1] is incompatible with number [2] in property `field`.
References:
4:   field: ?number            ^ [1]
9: function func(arg: {field: number}){
                              ^ [2]

Run online in the Try Flow

Two questions:

  1. Why doesn't it work?
  2. What is best practice to make it work?

The code snippets above are simplified version of the code to demonstrate the problem. Actual obj is more complex.

like image 420
Paker Avatar asked Nov 18 '25 09:11

Paker


1 Answers

First-off, Flow's refinement applies to individual fields here, but generally you aren't refining the whole object's type, so you can't refine an object's type based on refinement of a single field (as far as I'm aware). Even if you could, this example would still not be typesafe. Here's something that illustrates the problem with allowing this:

let obj: { field: ?number } = { field: 1 };

if(obj.field !== null && obj.field !== undefined){
    func(obj);

    obj.field = null;
}

function func(arg: {field: number}){
    setTimeout(() => {
      var value: number = arg.field;
    }, 10);
}

Given purely the restrictions you're looking for, this would typecheck, but the value inside the func timeout callback would actually be null.

The only way your given code could be safe with typechecking is if the arg.field value were marked contravariant (note the - in the property name for func now, e.g.

let obj: { field: ?number } = { field: 1 };

if(obj.field !== null && obj.field !== undefined){
    func(obj);
}

function func(arg: {-field: number}){
  arg.field = 43;
}

(On Flow Try)

This effectively would make arg.field write-only, which is fine because writing a number to field would always be safe, but reading one is not safe in the long term.

What is best practice to make it work?

It depends on what your real code does, to some extent. In this specific example I'd say pass the number directly as the argument, but assuming it needs to be an object, you could consider creating a new object with just the properties func needs.

like image 177
loganfsmyth Avatar answered Nov 20 '25 21:11

loganfsmyth