I am interested in switching my Flow code to strict
type checking, but I have some low-level utility functions that deal with Objects generically, such as:
// @flow strict
const hasKey = (o: Object): (string => boolean) =>
Object.prototype.hasOwnProperty.bind(o);
const union = (os: Array<Object>): Object =>
os.reduceRight((acc, o) => ({ ...acc, ...o }), {});
Since the Object type isn't allowed in strict mode, how do you declare types for functions that are explicitly supposed to operate on any generic Object?
You'll actually be benefiting greatly from stricter typing, in this case. By using Object
you are essentially turning the typing system off for all data that passes through these functions until they can be explicitly re-typed elsewhere. This means that you're currently losing a ton of typing information that you don't need to.
This is a textbook case for generics, which are documented here.
// @flow strict
const hasKey = <T: {}>(o: T): (string => boolean) =>
Object.prototype.hasOwnProperty.bind(o);
const union = <T: {}>(objects: Array<T>): T =>
objects.reduce((acc, o) => ({ ...acc, ...o }), ({}: $Shape<T>));
The most important part of the above is the : {}
in <T: {}>
. These are type bounds. If a generic is a way of saying, "allow the user to pass any type they want, and store that type in a variable so that I can refer to it later," then type bounds are a way of saying "allow the user to pass any type they want, as long as that type is a member of type X."
Because of the way width subtyping works, {}
is the most generic object type. Effectively all objects are subtypes of {}
. So <T: {}>
basically means, "T should be any type that is an object."
Note that this is very different from <T: Object>
, which basically means, "T is an object and from now on I'm not checking anything else about it." This means that we can do things like:
const o: Object = {};
console.log(o.some.member.that.doesnt.exist); // no error at compile time,
// but obvious error at runtime
Unlike:
const o: {} = {};
console.log(o.member); // Error, we don't know about this member property!
So by telling flow that the argument is a subtype of {}
, we are telling it that it has the basic API of an object. It has properties, it can be rest and spread, it can be string indexed, etc., but nothing else. Additionally, by storing the data type as the generic T
and returning that type, we are maintaining the type information of the argument. This means that whatever we pass in as an argument, we will get the same type of thing out the other side (instead of a black box of mystery).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With