Passing exact subelements to inexact object in function fails - flowtype

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!

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
};
});

FlowJS and function with 2 signatures and optional parameter

How do I precisely type in Flow the following function that either takes a callback and call it later with some value or takes no arguments and return a Promise for that value?
const foo = callback => {
const p = Promise.resolve(1.0);
if (callback === undefined) {
return p;
}
p.then(callback);
}
};
I tried to use an intersection type like in:
type CallbackCase = ((number) => void) => void;
type PromiseCase = () => Promise<number>;
const foo: CallbackCase & PromiseCase =
callback => {
const p = Promise.resolve(1.0);
if (callback === undefined) {
return p;
}
p.then(callback);
};
But then Flow complains (this is also so with the latest version at https://flow.org/try):
Cannot assign function to `foo` because undefined [1] is incompatible with `Promise` [2] in the return value. [incompatible-type]
So how can I fix this?
Update: The reason for this signature is that we have older code where the function was taking a callback. We would like to convert it to the promise form while still supporting older callers. During the conversion we would like to keep the types precise. In particular at the call site only the two forms should be allowed:
let a: Promise<number> = foo();
foo(callback);
Any other forms should be rejected.
You can specify argument as a function
type Callback = <T>(T) => void;
const foo =
(callback:Callback) => {
const p = Promise.resolve(1.0);
if (callback === undefined) {
return p;
}
p.then(callback);
};
seems to works well

Flow property is missing in mixed passed - despite the type annotation

I have the following in my declarations file (included in my [libs]):
export type EtlFieldNoIdxT = {
name: Name,
purpose: Purpose,
}
export type EtlFieldT = {
idx: number,
...EtlFieldNoIdxT
}
And the following in my use of the types:
export const createEtlField = (
etlFields: { [Name]: EtlFieldT },
newField: EtlFieldNoIdxT,
) => {
if (etlFields === {}) {
throw new Error({
message: 'Cannot create a new etlField with an empty etlFields',
});
}
const field: EtlFieldT = {
idx: maxId(etlFields, 'idx') + 1,
...newField,
};
const subject: Name = Object.values(etlFields).find(
(f) => f.purpose === 'subject', // <<< f.purpose "missing in mixed" error
).name; // <<< .name "missing in mixed" error
return newEtlField(field, subject);
};
Despite having annotated the input, can flow not infer the type of what Object.values would thus return?
Thank you in advance for pointing out my misunderstanding.
- E
If you check the declaration for Object.values you'll find that it returns an array of mixed:
static values(object: $NotNullOrVoid): Array<mixed>;
A quick google search came back with
https://davidwalsh.name/flow-object-values
So to solve your issue, you wrap Object.values(...) with any, and then inside your find arg you can type it as EtlFieldT and finally refine your type back to EtlFieldT after find.
const subject: Name = ((Object.values(etlFields): any).find(
(f: EtlFieldT) => f.purpose === 'subject',
): EtlFieldT).name;
Though you should be aware that find has the possibility of returning undefined. So to be sound, you should run the find, and declare subject if the value exists.

How to type annotate "function wrappers" (function which returns a function with the same signature as it's argument)

Is there a way to properly tell flow that I'm returning a function with the same signature as the function I'm passed, but not exactly the same function ?
This is an example of a "once" wrapper which prevents a function from being called multiple times, it works but uses an any-cast internally to make flow give up, I'd like to get rid of that cast and have 100% coverage:
module.exports.once = /*::<F:Function>*/(f /*:F*/) /*:F*/ => {
let guard = false;
return ((function () {
if (guard) { return; }
guard = true;
return f.apply(null, arguments);
}/*:any*/) /*:F*/);
};
Okay, first things first.
Your return value can currently never match F without your casting through any because the signature of the function you're returning is not the same because it can return undefined where the original may not.
(comment syntax removed for readability)
module.exports.once = <F: Function>(f: F): F => {
let guard = false;
return ((function () { // this function returns the return value of F or void
if (guard) { return; } // returning void
guard = true;
return f.apply(null, arguments);
}: any): F);
};
But to start typing this, we're gonna need to break down that function generic a little bit.
First of all, let's not use Function as it's generally better if we don't:
However, if you need to opt-out of the type checker, and don’t want to go all the way to any, you can instead use Function. Function is unsafe and should be avoided.
Also, we're going to extract the types of the arguments and the return value so we can manipulate them independently and construct a return type. We'll call them Args and Return so they're easy to follow.
module.exports.once = <Args, Return, F: (...Array<Args>) => Return>(
f: F
) ((...Array<Args>) => Return | void) => { // note `Return | void`
let guard = false;
return function () {
if (guard) { return; }
guard = true;
return f.apply(null, arguments);
};
};
Now that we're taking into account that our new function might return void everything type checks fine. But of course, the return type of our once function will no longer match the type of the passed function.
type Func = (number) => string;
const func: Func = (n) => n.toString();
const onceFunc: Func = module.exports.once(func); // error!
// Cannot assign `module.exports.once(...)` to `onceFunc` because
// undefined [1] is incompatible with string [2] in the return value.
Makes sense, right?
So, let's discuss the signature of this function. We want our return value to have the same signature as the function we pass in. Currently it doesn't because we're adding void to the signature. Do we need to? Why are we returning undefined? How can we always return the same type from our onced function? Well, one option would be to store the return value from the single call to the function and always return the stored return value for subsequent calls. This would kind of make sense because the whole point is to allow multiple calls but not perform any of the functions effects. So this way we can avoid changing the interface of the function, so we really don't need to know whether or not the function has been called before.
module.exports.once = <Args, Return, F: (...Array<Args>) => Return>(
f: F
): ((...Array<Args>) => Return) => {
let guard = false;
let returnValue: Return;
return function () {
if (guard) { return returnValue; }
guard = true;
returnValue = f.apply(null, arguments);
return returnValue;
};
};
type Func = (number) => string;
const func: Func = (n) => n.toString();
const onceFunc: Func = module.exports.once2(func);
One good question to ask at this point would be, why do the types match even if we're not technically returning exactly F? The answer to that is because functions in flow are structurally typed. So if they have the same arguments and return value, their types match.

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'
}

Resources