Skip to content

Instantly share code, notes, and snippets.

@asolove
Last active April 21, 2016 14:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save asolove/17158bba9d30242ee509e37c7a0843d8 to your computer and use it in GitHub Desktop.
Save asolove/17158bba9d30242ee509e37c7a0843d8 to your computer and use it in GitHub Desktop.
Nested subtyping
// @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
Copy link

  1. Does widgets always of length 1? [Widget] is a tuple with 1 element. Array<Widget> or Widget[] is an array of Widgets

  2. Long story short, an Array<Bunny> is not a subtype of Array<Animal>. Consider

    var arrayOfBunnies: Array<Bunny> = [Flopsy, Mopsy, Cottontail, Peter]; // Ok
    var arrayOfAnimals: Array<Animal> = arrayOfBunnies; // If we allowed this...
    arrayOfAnimals.push(MrMcGregor); // then you could add a human to the list of bunnies!

    If 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 a Widget into a [ManufacturableWidget].

  3. But never fear! All is not lost! You can use generics to solve this problem!

    // Adding generics to Batch lets us say that
    // ManufacturableBatch is a subtype of Batch<ManufacturableWidget>
    type Batch<W: Widget> = { id: number, widgets: [W] }
    
    // And add generics here too...
    function batchCustomers<W: Widget>(b: Batch<W>): [number] { ... }

    And that should do it!

@asolove
Copy link
Author

asolove commented Apr 21, 2016

@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