Do flow refinements not propagate "up"? - flowtype

I was suprised to see that this example doesn't typecheck:
/* #flow */
type State = { flag: boolean }
function firstStep(state: State) {
if (state.flag) {
secondStep(state)
// this works though:
// secondStep({ flag: state.flag })
}
}
function secondStep(state: { flag: true }) {}
3: type State = { flag: boolean }
^ boolean. Expected boolean literal `true`
13: function secondStep(state: { flag: true }) {}
^ boolean literal `true`
Flow knows it can refine state.flag to true, but it doesn't know that state can be refined to { flag: true }. Is that expected?

As with many subtype relationships that look fine on the surface, this is ruined by mutability. secondStep could retain a reference to state, and firstStep could later change state.flag to false.
However, this does work if you use disjoint unions:
type State = { flag: true } | { flag: false };
(tryflow)
You may find that disjoint unions suit your use case better anyway, since then you can have different properties in your State object depending on the value of the flag.
Note that in this case, Flow does not allow you to set state.flag to false, so the refinement can hold in secondStep.

Related

Function taking a function returning a generic type is not typesafe

I experience this when using React hooks, but it is a general TypeScript question.
You can see it in the playground
When I use the generic parameter which I finds most intuitive the function I pass in isn't completely typesafe. As long as it returns an object with the props in T the compiler is happy, but I can also add props that are not part of T. This becomes a problem when T has some optional props, because I can miss spell them and not know it.
If I instead set the return type explicit on the function I pass in, everything works as expected, but this is not an intuitive usage when the function I call have a generic parameter.
Can someone please explain why the compiler is allowing this?
const foo = <T>(f: ()=>T) => {
return f();
};
type Result = {
bar: number;
foo?:number;
}
const strange = foo<Result>(() => {
return {
bar: 42,
baz: 12, // why is this prop allowd?
FOO: 13 // ups I might think I set the foo prop, but I spelled it wrong
};
});
const expected = foo((): Result => {
return {
bar:42,
FOO: 12 // this is not allowd, which is what I wxpected
};
});
Excess property check is triggered only for "fresh" object literal.
The freshness of an object literal is lost in case of type assertion and type widening.
in strange function, there is a type widening on the return type (see issue #241). So excess properties are valid.
const strange = foo<Result>(() => {
// type widening occurs here
// freshness is lost
// No excess property check triggered
return {
bar: 42,
baz: 12, // excess property allowed
FOO: 13 // excess property allowed
};
});
in the expected function, there is no type widening since the return type is explicitly specified. the freshness of the object literal is maintained, and the excess property check is triggered
const expected = foo((): Result => {
// No type assertion
// object literal is fresh
// Excess property check is triggered
return {
bar:42,
FOO: 12 // this is not allowd, which is what I wxpected
};
});

Passing exact subelements to inexact object in function fails

I want to pass an object with an exact object parameter to my function without having to specify an exact match:
/* #flow */
type a = {|
b: string,
c: number,
d: {| e: number, f: string |},
|}
interface props { b: string, d: { e: number } }
function foo(x: props) {
console.log(`${x.b}: ${x.d.e}`);
}
let var_a: a = {
b: 'b',
c: 0,
d: { e: 1, f: 'f' }
};
foo(var_a)
Unfortunately flow 0.78.0 gives:
19: foo(var_a)
^ Cannot call `foo` with `var_a` bound to `x` because inexact object type [1] is incompatible with exact object type [2] in property `d`.
References:
8: interface props { b: string, d: { e: number } }
^ [1]
5: d: {| e: number, f: string |},
^ [2]
19: foo(var_a)
^ Cannot call `foo` with `var_a` bound to `x` because property `f` is missing in object type [1] but exists in object type [2] in property `d`.
References:
8: interface props { b: string, d: { e: number } }
^ [1]
5: d: {| e: number, f: string |},
^ [2]
I have also tried using type instead of interface:
type props = { b: string, d: { e: number } }
Now this can easily be fixed by specifying the d to be an exact element:
type props = { b: string, d: {| e: number, f: string |} }
This is rather annoying though as I would like to specify a minimum number of parameters in my function, i.e the f parameter is never used in foo and should therefore in my mind not be a requirement.
You can find the code in Try Flow here
I experimented this further and determined why it happens, but I am not sure I am happy with the answer.
AFAICT this is a quirk of function subtypes only.
// #flow
const Clapper = (opts:{style:string}) => { console.log('clapping '+opts.style); };
const InvokeClapper = (fn:({})=>void) => { fn({}) };
InvokeClapper(Clapper); // fails :(
const Clapper2 = (opts:{}) => { console.log('clapping'); };
InvokeClapper(Clapper2); // works!
Clapper({}); // works!
Clapper2({}); // works!
const a = (b:{}) => 1;
a({}); // works
a({style:'string'}); // works
const a2 = (b:({a:number})=>void) => 1;
const a3 = (b:({|a:number|})=>void) => 1; // this and the above behave the same. why does it assume exact type for function param?
a2( (b:{a:number})=>{} ); // works
a2((b:{a:number,style:string})=>{}); // fails :(
const c2 = (b:({[string]:any})=>void) => 1;
c2((b:{a:number,style:string})=>{}); // works
const c3 = (b:(any)=>void) => 1;
c3((b:{a:number,style:string})=>{}); // works
const c4 = (b:({})=>void) => 1;
c4((b:{a:number,style:string})=>{}); // fails
// how can i say: a function that takes any object?
// or a function which takes any subtype of this object? any object more specific than { a:string, ... }
// this is the textbook example from Flow docs Width Subtyping page; very similar
const c5 = (b:{foo:string}) => 1;
c5({foo:"test", bar:42}); // works!
const c6 = (b:({foo:string})=>void) => 1;
// i guess this is a type=>type comparison, not a value=>type comparison
c6((x:{foo:string,bar:number})=>{}); // fails
c6((x:{foo:string})=>{}); // works
// this is one solution which seems acceptable but i am still confused why is necessary?
const c7 = (b:(x:{foo:string,[string]:any})=>void) => 1;
// what I want to express is: function b promises, for its x object parameter,
// to read/write only on x.foo.
// or, if it attempted to read/write other keys, that should be a compile-time error.
c7((x:{foo:string,bar:number})=>{}); // works
// since exact match seems to do nothing here, i almost wish we could change its meaning,
// so that instead of forcing you to pass a variable with no additional keys,
// it instead forced the function b to not attempt to access or write to keys other than those in the exact match.
const c8 = (b:({|foo:string|})=>void) => 1;
c8((x:{foo:string,bar:number})=>{}); // fails
const altc8: ({foo:string})=>void = (x:{foo:string,bar:number})=>{}; // fails; but using lovely shorter syntax from docs
// reading chapter "Subsets & Subtypes" > "Subtypes of functions"
// it seems like, as in the doc example function f3,
// "The subtype must accept at least the same inputs as its parent,
// and must return at most the same outputs."
const c9 = (b:({foo:string,bar:number})=>number|void) => 1; // this is the parent
c9((x:{foo:string})=>{return undefined;}); // works // so this is the subtype
// i dislike this though.
// from my perspective, it shouldn't matter how many keys on the object in parameter 1.
// they should just both be considered an inexact object of any number of keys.
// while the parent considers it a sealed/exact object of just one key [that it cares about]. arrgh...
// going with solution c7 for now
see:
https://flow.org/en/docs/lang/subtypes/#toc-subtypes-of-functions
UPDATE:
I later discovered the next problem is that functions in an object type are automatically considered read-only/covariant (ie. { +yourFn: ()=>void }), whereas the {[string]:any} map type automatically marks all keys read/write. So if your subtype includes functions, it will be rejected because the functions are read-only in the subtype but read/write in the parent type.
I worked around THAT by unioning generics in the parent--as an alternative to the map type--and, specifying generic parameters as necessary on function invocation.
ie., instead of:
const fn: ({ a:number, [string]: any })=>void = (x:{ a:number, b:number })=>{};
fn({a:1, b:2});
i went with:
const fn2 = <V>(x:{a:number} & V)=>{};
fn2<{b:number}>({ a:1, b:2 });
So, delaying the problem until invocation. It still provides a compile-time check. Working well so far. It seems very powerful! The Flow type system is stronger than in Java. What a twist!

Need clarification about flowtype "exact Union Types"

I don't understand how Unions work.
Doc reference
see this doc example about exact Union Types
Problem
Code below will throw a flow error on item.rocks:
/* #flow */
type MoutainType = {|
rocks: boolean,
|};
type OceanType = {|
waves: boolean,
|};
type HolidayType = MoutainType | OceanType;
const haveHoliday = (item: HolidayType) => {
return item.rocks; //----------------> Error (but shouldn't)
}
Try yourself
See live demo
You have defined HolidayType to be an union of the types MountainType or HolidayType. Flow needs to be able to determine which type it is dealing with before it will allow you to access an exclusive member property without throwing an error.
If you test for the rocks property before attempting to access it, Flow will then be able to determine which type is in play.
const haveHoliday = (item: HolidayType) => {
if (typeof item.rocks !== 'undefined') {
// Flow now knows that this must be a MountainType
return item.rocks;
}
}
Look at the docs for Disjoint Unions and see if there is a literal value that you can use as the selector for type, and that gives you a more natural code path e.g.
type MountainType = {
terrain: 'rocks'
}
type OceanType = {
terrain: 'waves'
}
type HolidayType = MountainType | OceanType
const haveHoliday = (item: HolidayType): boolean => {
return item.terrain === 'rocks'
}

Flow - reading a union type property doesn't work when caching the common variable check

In this example, Flow shows us a way to read properties depending on the type.
I have refactored it to look like this, and it works as expected:
type Success = { success: true, value: boolean };
type Failed = { success: false, error: string };
type Response = Success | Failed;
function handleResponse(response: Response) {
const value = response.success && response.value;
const error = !response.success && response.error;
}
However, when the common property is a string, it works when doing a === check, but it doesn't if you cache the check into a variable:
type Success2 = { success: 'success', value: boolean };
type Failed2 = { success: 'not_success', error: string };
type Response2 = Success | Failed;
function handleResponse(response: Response2) {
const isSuccess = response.success === 'success';
// const value = response.success === 'success' && response.value; // WORK
const value = isSuccess && response.value; // DOESN'T WORK
}
In other words, there must be a === check (literally) before reading the variable, can't have it in a variable.
Is this a known Flow limitation, or am I missing something?
Yep, this is expected behavior. The feature you are referring to here is type refinement. The implementation is based on the standard flow-control mechanisms of Javascript, meaning that saving the result of a test and using it later will discard the type information that Flow might otherwise be able to infer. All Flow knows in your non-working example is that isSuccess is a boolean, it has no idea that true there implies that response.value exists.

Strange type empty Flow syntax inside default statement of Redux reducer

I found the code sample while searching for ways to use flow with redux here:
https://flow.org/en/docs/frameworks/redux/
Peculiar syntax is (action: empty); Is it just a bit of flow magic intended to be used just inside default case of switch statement or does it have other uses?
It looks like out of place function type statement without return value type but with parameter of strange type 'empty', which I couldn't find documentation about.
// #flow
type State = { +value: boolean };
type FooAction = { type: "FOO", foo: boolean };
type BarAction = { type: "BAR", bar: boolean };
type Action = FooAction | BarAction;
function reducer(state: State, action: Action): State {
switch (action.type) {
case "FOO": return { ...state, value: action.foo };
case "BAR": return { ...state, value: action.bar };
default:
(action: empty);
return state;
}
}
empty is Flow's bottom type. I believe the main motivation for its initial introduction was symmetry but it has proven to have some uses. As you have identified it can be used in this case to make Flow enforce exhaustiveness. It can be used similarly in a chain of if/else statements.
However, it can be used anytime when you want Flow to prevent any actual value from ending up somewhere. This is very vague, so here are a couple examples:
// Error: empty is incompatble with implicitly-returned undefined
function foo(): empty {
}
// No error since the function return is not reached
function foo2(): empty {
throw new Error('');
}
function bar(x: empty): void {
}
// Error: too few arguments
bar();
// Error: undefined is incompatible with empty
bar(undefined);
In the foo examples, we can see that Flow enforces that a return is never reached in a function returning empty. In the bar example, we can see that Flow prevents the function from being called.

Resources