Last active
April 21, 2016 14:10
-
-
Save asolove/17158bba9d30242ee509e37c7a0843d8 to your computer and use it in GitHub Desktop.
Nested subtyping
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @flow | |
// I want to write some shared logic with the 'core' data for an object: | |
type Widget = { id: number, customer_id: number } | |
function getCustomerId(w: Widget): number { | |
return w.customer_id | |
} | |
// But in another context Widgets have some extra information | |
type ManufacturableWidget = { id: number, customer_id: number, machine_id: number } | |
// I often write them as Union types. I took that out in this case in case that complicated things, but it looks like: | |
// type ManufacturableWidget = Widget & { machine_id: number } | |
// That still plays nice with the shared logic: | |
const w: ManufacturableWidget = { id: 1, customer_id: 2, machine_id: 3 } | |
getCustomerId(w) // 2; works fine | |
// Using base and extended types like this has been pretty helpful for me. | |
// Lets me prove I meet some shared contract but also add extra data in for the context I want. | |
// But here's the challenge: it doesn't work for subtyping nested object properties. | |
// Let's say I have some shared logic on an object that has a nested shared type: | |
type Batch = { id: number, widgets: [Widget] } | |
// And some shared logic on that object: | |
function batchCustomers(b: Batch): [number] { | |
return b.widgets.map(getCustomerId); | |
} | |
// And another context where I have that plus some extra info and want a more specific subtype of widget: | |
type ManufacturableBatch = { id: number, priority: number, widgets: [ManufacturableWidget] } | |
let mb: ManufacturableBatch = { id: 1, priority: 2, widgets: [w] }; | |
batchCustomers(mb); | |
// It doesn't work. :() | |
/* | |
I know a little about co- and contra-variance on subtyping, but in this case we're purely taking in a subtyped argument, there's no mutation or output. | |
So naively, to someone who knows nothing about type theory or Flow's internals, it seems like it should work? | |
ManufacturableWidget subtypes to Widget, so shouldn't an object with a ManufacturableWidget property subtype to one with a Widget property? | |
But it doesn't. Any suggestions on how to better model my types so that I can have a shared core plus extra data in other contexts with nested data? | |
The error is: | |
subtype.js:34 | |
34: batchCustomers(mb); | |
^^^^^^^^^^^^^^^^^^ function call | |
27: type ManufacturableBatch = { id: number, priority: number, widgets: [ManufacturableWidget] } | |
^^^^^^^^^^^^^^^^^^^^ property `machine_id`. Property not found in | |
24: type Batch = { id: number, widgets: [Widget] } | |
^^^^^^ object type | |
*/ |
@gabelevi: thanks! I can't believe I've been using tuple syntax for all my arrays. Blarg.
I was hoping variance wasn't a problem because all of my data was immutable, but sounds like mutation analysis isn't coming for a little while.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Does
widgets
always of length 1?[Widget]
is a tuple with 1 element.Array<Widget>
orWidget[]
is an array ofWidgets
Long story short, an
Array<Bunny>
is not a subtype ofArray<Animal>
. ConsiderIf JavaScript arrays were immutable, then there would be no problem. But their mutable, so this is a danger! Tuples have the same problem. If we say that
[ManufacturableWidget]
is a subtype of[Widget]
, then you could write aWidget
into a[ManufacturableWidget]
.But never fear! All is not lost! You can use generics to solve this problem!
And that should do it!