Properly typing generic object property access - flowtype

I have a bit of JS that I've removed all of the unimportant bits from, and I'm trying to add Flow types to it:
function doStuff(key, data, callback) {
callback(data[key]);
}
In this case, data is a non-disjoint union of about 30 different object types and callback is a function that expects exactly one of those types.
My initial thoughts were to type it like this:
function doStuff2<TKey: $Keys<Data>>(key: TKey, data: Data, callback: ($ElementType<Data, TKey>) => void): void {
callback(data[key]);
}
An example of what Data might look like:
type Data = {|
a: {| username: string, password: string |},
b: {| alert: string |},
c: {| prompt: string, primaryAction: ActionItem, secondaryAction: ActionItem |},
d: {| validCountries: Array<Country> |},
/* and so on... */
|};
However, this doesn't work. If the Data type is just {| a: number, b: string |}, then I get errors that number is incompatible with string and string is incompatible with number.
My questions are:
Why doesn't this work? What type is $ElementType<Data, TKey> and why is it incompatible with typeof data[key]?
What can I do to fix this without changing the type of Data?

Related

How do I spread the $Shape of an object into an Exact object

I'm trying to spread an object definition provided by a third party library as optional properties on my exact object type.
Example:
type Match = {
params: { [key: string]: ?string, ... },
isExact: boolean,
path: string,
url: string,
...
}
// Modified version of react-router-dom ContextRouter type for example purposes
type ContextRouter = {|
history: string,
location: string,
match: Match,
|};
// Not working how I want it to
type Props = {|
...$Shape<ContextRouter>,
match: { params: { [key: string]: ?string, ... } }
|};
The end goal is to allow these scenarios to work correctly:
// I want this to be allowed (with history and location optional)
({
match: {params: {id: '7'}}
}:Props);
// I want this to throw an error on the 'unknown' property
({
match: {params: {id: '7'}},
unknown: true
}:Props);
I want everything in ContextRouter to be optional. My example currently throws errors if any of the properties of ContextRouter are missing.
22: ({ ^ Cannot cast object literal to `Props` because property `history` is missing in object literal [1] but exists in `Props` [2].
References:
22: ({ ^ [1]
24: }:Props);
^ [2]
I made a starting point here: flow playground
You can do it with the $Rest utility type
From docs:
$Rest<{|n: number|}, {}> will result in {|n?: number|} because an in-exact empty object may have an n property
Also, $Shape is broken with spreads.
Try here

Functions for constructing Flow types?

Is there any way I can do something like this:
// #flow
function FailureActionType(name: string): Type {
return {type: name, error: string}
}
type SearchFailureAction = FailureActionType("SEARCH_FAILURE")
Obviously there's problems in the way the typing/assignments are written in the return statement, but it would work such that
type SearchFailureAction = { type: "SEARCH_FAILURE", error: string }
Is there any way to do that?
You want a generic.
type FailureActionType<T: string> = { type: T, error: string }
The <T> there says that this type is dependent on another type.
<T: string> means this dependent type must be a type of string.
{ type: T, error: string } means the resulting type must have the dependant type on the type key of the object.
You use it by passing in a value for T in <> like so:
type SearchFailureAction = FailureActionType<"SEARCH_FAILURE">
const action1: SearchFailureAction = { type: 'SEARCH_FAILURE', error: 'some error' }
const action2: SearchFailureAction = { type: 'BAD', error: 'some error' } // type error
flow.org/try Proof
Generics are pretty powerful. Read the docs for more.
https://flow.org/en/docs/types/generics/

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!

Indexable signature not found in object literal in Flow

I have this interface:
interface IFormData {
[string]: string
};
export type { IFormData };
It's simple interface that accepts key-value only string. But when I use this,
const formData:IFormData = { email: '...', password: '...' };
it gives me this error:
[flow] property $key of IFormData (Indexable signature not found in object literal)
I also tried this, but it gives me same error:
var formData: IFormData; // Error
formData['email'] = ...;
formData['password'] = ...;
I searched this on google almost 2 days, but still stuck in here and I need some help!
Any advice will very appreciate it.
If you switch from interface to type, Flow seems to be a lot happier with this sort of thing:
type IFormData = {
[string]: string
}
const formData: IFormData = { email: '...', password: '...' };
I'm guessing the fact that interfaces are nominally typed, rather than structurally typed, has something to do with this.

Why is TypeScript not letting me query a Meteor collection?

I'm using the packages meteortypescript:compiler and meteortypescript:typescript-libs. I'm trying to query a collection for a record like this:
MyCollection.findOne({email: 'user#example.com', username: 'bob'});
And I'm getting this error:
error TS2345: Argument of type '{ email: string; username: string; }'
is not assignable to parameter of type 'Selector'.
This makes no sense, because I can see that the type Selector in the .d.ts file I'm using for Meteor is defined as interface Selector extends Object {}, which should not be giving a problem. Any pointers?
Combine interface Selector extends Object {} with Argument of type '{ email: string; username: string; }' is not assignable to parameter of type 'Selector'. leads me to think you have a freshness aka strict object checking problem. Here is a simplified example:
interface Selector extends Object {}
function foo(arg:Selector){}
foo({ email: 'asfd#asdf.com', username: 'asdf' }); // same error you are getting

Resources