Hey Guys,
As always thanks for your continued efforts. I run into situations fairly often where I end up wanting to represent a type in a "DeepMaybe" situation. Generally I try to make all my types "Exact" types and make my types as strict as possible.
$Shape<> is great and helps al ot but it only makes the values optional one level deep.
export type State = {|
ip: string,
servername: string, // What is the clients servername ?
handshaked: boolean, // Has this client handshaked?
version?: string,
apikey?: string,
headers: {
[header: string]: string,
},
connection: {
disconnecting: boolean,
lastPingSent: number,
lastPongReceived: number,
pingsSent: number,
pongsReceived: number,
},
|};
This works great. Then I can represent the "shape" of the allowed state in a setState command such as $Shape<State>
While this will make all the first level keys optional, it keeps any values in a deeper level as required (and definitely how I think it should work by default).
However, in many cases we either want to take a mutation of that object and/or operate on an object that should "look" like our final state but won't necessarily be exact.
This forces us to create another type with all optional properties which quickly becomes extremely cumbersome.
I believe there is a strong precedent for a $DeepShape<> which would make all properties in the object optional including any embedded types which are also objects.
I guess the place that $DeepShape should probably not operate on woudl be opaque types since that wouldn't make much sense anyway? (or maybe it could since I also find myself doing $Shape<{ ...OpaqueType }> a lot so that I can take in a shape of the opaque type and return the opaque type.
Anyway, would love to see this at some point if at all possible! Open to ideas if there is another good way of handling this that doesn't quickly become cumbersome. Obviously could just make the whole object one-level deep but that kills things in many situations.
In theory, you can implement this yourself with $ObjMap:
type $DeepShape<O: Object> = $Shape<$ObjMap<O, <V>(V) => $Call<typeof _deepShape, V>>>;
declare function _deepShape<O : Object>(O) : $DeepShape<O>;
declare function _deepShape<V>(V) : V;
type Nested = {
foo : string,
bar : string,
baz : {
foo : string,
bar : string
}
};
type NestedShape = $DeepShape<Nested>;
var x : NestedShape = {
foo : 'foo',
bar : 'bar',
baz : {
foo : 'foo'
}
};
// No errors!
Unfortunately this doesn't quite work in practice: the above snippet compiles just fine, but as soon as you introduce more than one NestedShapes then flow starts spitting out errors. Maybe someone knows how to fix that?
Yeah, the other option is to build up the types without exact and all required then use $Exact<> and $Shape<> to refine it at each level but that becomes cumbersome quite quickly.
There is no doubt $ObjMap is powerful in these situations but I would think some of these make sense as included utilities (even if just written in pure-flow and not via ocaml).
managed to get this working using $ObjMap
type $DeepShape<O: Object> = $Shape<
$ObjMap<O, (<V: Object>(V) => $DeepShape<V>) | (<V>(V) => V)>
>
@cloudkite it looks to work pretty well, thank you. I get cleaner error messages when I make the second parameter of the $ObjMap an intersection instead of a union. It doesn't appear to make a difference in the type checking. Also, intersecting with Object will prevent null and void from being accepted: (Try Flow)
type $DeepShape<O: Object> = Object & $Shape<
$ObjMap<O, (<V: Object>(V) => $DeepShape<V>) & (<V>(V) => V)>
>;
type Nested = {
foo : string,
bar : string,
baz : {
foo : string,
bar : string
}
};
const passing: $ReadOnlyArray<$DeepShape<Nested>> = [
{},
{ baz: {} },
{ baz: { foo: '' } }
]
const failing: $ReadOnlyArray<$DeepShape<Nested>> = [
null, // only if you intersect with Object
{ wat: 1 },
{ baz: 1 }
]
@jcready Thanks for the improvements!
@bradennapier you can make this globally available by adding this into a file referenced by your .flowconfig [libs] section or just put it inside your flow-typed dir
// flow-typed/util.js
type $DeepShape<O: Object> = Object & $Shape<
$ObjMap<O, (<V: Object>(V) => $DeepShape<V>) & (<V>(V) => V)>
>;
I've found something that may be a limitation of the currently implementation (Try Flow).
If you have a type that intersects with another type, then doing $DeepShape on it makes it impossible to type check.
@AndrewSouthpaw you can use type spreading instead see try flow
however there are a bunch of know issues with spreading types so you could potentially be trading one problem for another
Interesting. I'm less familiar with that strategy, what are some of the tradeoffs? We've been struggling to come up with a reasonable strategy for subtyping.
We switched from intersection to spreading because recompose types use spread. It鈥檚 also more flexible because it essentially allows one to override types by spreading a type and then redeclaring it below in the object
A couple caveats:
You must $Exact the type of it isn鈥檛 exact before spreading or else anything else in the object becomes optional because there鈥檚 no guarantee that the object will not overwrite those other types.
Spreading sometimes does not play nicely itself with intersection I find.
That being said, we have a mostly working type system with recompose using object spread to compose types. I would also recommend using invariant types because that solves some problems with optional types.
Using a function union fails to catch the following error, whereas an intersection catches it:
type $DeepShape<O: Object> = Object & $Shape<$ObjMap<O, (<V: Object>(V) => $DeepShape<V>) | (<V>(V) => V)>>
type Nested = {
foo : string,
bar : string,
baz : {
foo : string,
bar : string
}
};
const failing: $ReadOnlyArray<$DeepShape<Nested>> = [
{ baz: {foo: 2} }
]
Also issues with objects inside arrays:
type $DeepShape<O: Object> = Object & $Shape<$ObjMap<O, (<V: Object>(V) => $DeepShape<V>) | (<V>(V) => V)>>
type Baz = {|
foo : string,
bar : string
|}
type Nested = {|
foo : string,
bar : string,
baz : Baz[]
|};
const failing: $DeepShape<Nested> = {
baz: [
{foo: '2'}
]
}
Cannot assign object literal to
failingbecause propertybaris missing in object literal [1] but exists inBaz[2] in array element of property `baz
Would be swell to have native support for this. 馃槙
Most helpful comment
@cloudkite it looks to work pretty well, thank you. I get cleaner error messages when I make the second parameter of the
$ObjMapan intersection instead of a union. It doesn't appear to make a difference in the type checking. Also, intersecting withObjectwill preventnullandvoidfrom being accepted: (Try Flow)