Search code examples
covarianceflowtype

Typing a "camel caser" in Flow: variance issues?


try flow link

I've been messing around with typing a "camel caser" function (one that consumes JSON and camel cases its keys). I've run into a few issues along the way, and I'm curious if y'all have any suggestions.

A camel caser never changes the shape of its argument, so I would like to preserve the type of whatever I pass in; ideally, calling camelize on an array of numbers would return another array of numbers, etc.

I've started with the following:

type JSON = null | string | number | boolean | { [string]: JSON } | JSON[]
function camelize<J: JSON>(json: J): J {
    throw "just typecheck please"
}

This works great for the simple cases, null, string, number, and boolean, but things don't quite work perfectly for JSON dictionaries or array. For example:

const dictionary: { [string]: number } = { key: 123 }
const camelizedDictionary = camelize(dictionary)

will fail with a type error. A similar issue will come up if you pass in a value of, say, type number[]. I think I understand the issue: arrays and dictionaries are mutable, and hence invariant in the type of the values they point to; an array of numbers is not a subtype of JSON[], so Flow complains. If arrays and dictionaries were covariant though, I believe this approach would work.

Given that they're not covariant though, do y'all have any suggestions for how I should think about this?


Solution

  • Use property variance to solve your problem with dictionaries:

    type JSON = null | string | number | boolean | { +[string]: JSON } | JSON[]
    

    https://flowtype.org/blog/2016/10/04/Property-Variance.html

    As for your problem with Arrays, as you've pointed out the issue is with mutability. Unfortunately Array<number> is not a subtype of Array<JSON>. I think the only way to get what you want is to explicitly enumerate all of the allowed Array types:

    type JSON = null | string | number | boolean | { +[string]: JSON } | Array<JSON> | Array<number>;
    

    I've added just Array<number> here to make my point. Obviously this is burdensome, especially if you also want to include arbitrary mixes of JSON elements (like Array<string | number | null>). But it will hopefully address the common issues.

    (I've also changed it to the array syntax with which I am more familiar but there should be no difference in functionality).

    There has been talk of adding a read-only version of Array which would be analogous to covariant object types, and I believe it would solve your problem here. But so far nothing has really come of it.

    Complete tryflow based on the one you gave.