Flow: Declare a function which only returns null if argument is null?

Created on 4 Jul 2017  路  7Comments  路  Source: facebook/flow

I have something similar to the following code:

function test(v : ?string) {
    if (v == null) return null;
    return v + '!';
}

const res = test('a') + 'b';

Now flows gives "error(6, 13): Error: Flow: null. This type cannot be added to string" which is because it infers "test" returning something like"?string" thus the '+' operation wouldn't work.

But in the call it's guaranteed that 'v' is not null so the '+' operation would succeed. Is there a way to declare the function in a way that flow could infer that test gives a string if it's parameter is not null?

Most helpful comment

In the specific case of strings (or other primitives) you can do this with the help of bounded polymorphism:

function test<T : ?string>(input : T) : T {
  if (typeof input === 'string') {
    return input + '!';
  }
  return input;
}

const x : string = test('foo') + 'bar';
const y : null = test(null);
// $ExpectError
const a : string = test(null) + 'bar';
// $ExpectError
const b : string = test(null);
// $ExpectError
const c : null = test('foo');

For objects it's slightly less straightforward, since you can't manual instantiate a generic type. You can work around this by cloning the input object, and mutating the clone instead:

function test2<T : ?{ x : number }>(input : T) : T {
  if (typeof input === 'object') {
    return { 
      ...input,
      x : 1 
    };
  }
  return input;
}

All 7 comments

Why bother checking for null in the function when it only allows strings to be passed to it anyways?

Sorry, forgot a "?" at "string". Fixed it.

In the specific case of strings (or other primitives) you can do this with the help of bounded polymorphism:

function test<T : ?string>(input : T) : T {
  if (typeof input === 'string') {
    return input + '!';
  }
  return input;
}

const x : string = test('foo') + 'bar';
const y : null = test(null);
// $ExpectError
const a : string = test(null) + 'bar';
// $ExpectError
const b : string = test(null);
// $ExpectError
const c : null = test('foo');

For objects it's slightly less straightforward, since you can't manual instantiate a generic type. You can work around this by cloning the input object, and mutating the clone instead:

function test2<T : ?{ x : number }>(input : T) : T {
  if (typeof input === 'object') {
    return { 
      ...input,
      x : 1 
    };
  }
  return input;
}

Thank you, this works at least for some cases (even if it's a bit complicated for such relatively basic stuff).

But what about this:

function nextDay<T : ?Date>(date: T): T {
    if (date == null) return date;
    return new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate() + 1));
}

This gives the error "Flow: Date. This type is incompatible with the expected return type of some incompatible instantiation of T".

Now if I remove the Date.UTC call, it works (but of course it has different semantics now):

function nextDay<T : ?Date>(date: T): T {
    if (date == null) return date;
    return new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1);
}

Maybe something to do with type refinements?

The problem is that in the general case you cannot instantiate a new object for a generic type. You would need runtime type reflection for that, which doesn't exist with flow + js. Even with bounded polymorphism you can't do this, since you can always pass in a more specific type. E.g. suppose this was valid:

type CounterObj = { count : number };
function increment<T : CounterObj>(in : T) : T {
  return { count : in.count + 1 };
}

Then we run into problems when using subtypes of CounterObj:

type SubCounterObj = { count : number, somethingElse : number };
const myCounter : SubCounterObj = { count : 1, somethingElse : 0 };
// Valid according to the `increment` signature, but the return value isn't actually of type `SubCounterObj`.
const incremented : SubCounterObj = increment(myCounter);

This is why flow gives the error "This type is incompatible with the expected return type of some incompatible instantiation of T". I'm actually surprised that your last example does NOT give an error, because I'm quite sure it should: using a subtype of Date is valid according to the signature, but isn't actually safe to do.

I would suggest to instead split it up in two functions: one that only takes the non-nullable value (and thereby guarantees to return a non-nullable value), and one that takes a nullable value (and thus returns a nullable value). I think you should always be able to know in advance if your value is nullable or not, so I think you can always pick the right version.

There is no object instantiated for the generic type. The instantiation is quite explicit. I think that using bounded polymorphism for it is a bit hackish and it shows its limitations here.

While splitting it up (or used other additional code) is of course possible, it would require several rewrites of existing code etc. I don't consider that optimal.

In TypeScript this works (without creating any runtime checks):

function test(x: Date): Date;
function test(x: null): null;

function test(a) {
    if (a === null) return null;
    return new Date(Date.UTC(a.getFullYear(), 0, 0));
}

test(new Date()).getTime();        // ok
test(null).getTime();        // gives an error

You could do something somewhat similar in flow:

declare function test(x : Date) : Date;
declare function test(x: null) : null;
function test(a) {
    if (a === null) return null;
    return new Date(Date.UTC(a.getFullYear(), 0, 0));
}

test(new Date()).getTime();       //ok
test(null).getTime();  //error

But unfortunately this doesn't check if the implementation is consistent with the declaration.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

cubika picture cubika  路  3Comments

mmollaverdi picture mmollaverdi  路  3Comments

philikon picture philikon  路  3Comments

funtaps picture funtaps  路  3Comments

bennoleslie picture bennoleslie  路  3Comments