Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tagged Union of Types #50

Closed
DylanRJohnston opened this issue Jun 5, 2017 · 9 comments
Closed

Tagged Union of Types #50

DylanRJohnston opened this issue Jun 5, 2017 · 9 comments
Milestone

Comments

@DylanRJohnston
Copy link

DylanRJohnston commented Jun 5, 2017

I really love this library and was hoping to be able to write something similar to Haskell's generic functions. But to leverage totality checking I need to be able to form some kind of tagged union of the Types in this package. But I'm having some trouble.

I basically want something like this, where I can pass a Type object and a map of folds to handle each type of Type to build database schemas and stuff from the Type definition.

Any help would be greatly appreciated.

const assertNever = (x: never): never => {throw new Error(`Unexpected object ${x}`)}

type Types<A = any, B extends Props = {}, C extends Any[] = Any[], D = any> =
    | Type<A>
    | InterfaceType<B>
    | UnionType<C, D>

function genericFold<A = any, B extends Props = {}, C extends Any[] = Any[], D = any>(x : Types<A, B, C, D>, f: any) {
    if (x instanceof UnionType) {
        // Do something
        if (x.t instanceof Type) genericFold(x.t);
    } else if (x instanceof InterfaceType) {

    } else if (x instanceof Type) {

    } else {
        return assertNever(x);
    }
}
@gcanti
Copy link
Owner

gcanti commented Jun 6, 2017

Adding a _tag field to each *Type (a trick used in fp-ts) would allow for exhaustivity checks on custom defined unions

export class InterfaceType<P extends Props> extends Type<InterfaceOf<P>> {
+  readonly _tag: 'InterfaceType' = 'InterfaceType'
}

export class UnionType<RTS extends Array<Any>, U> extends Type<U> {
+  readonly _tag: 'UnionType' = 'UnionType'
}
type T = t.InterfaceType<any> | t.UnionType<any, any>

function f(type: T): string {
  switch (type._tag) {
    case 'InterfaceType':
      return 'interface'
    case 'UnionType':
      return 'union'
  }
}

@DylanRJohnston
Copy link
Author

Is there any downside / upside to using any for the type parameters? Instead of trying to preserve the type information?

@gcanti
Copy link
Owner

gcanti commented Jun 6, 2017

I don't think there's any downside. If needed, types can also be extracted

const Person = t.interface({
  name: t.string
})

// type A = { name: t.Type<string>; }
type A = typeof Person.props

const U = t.union([t.string, Person])

// type B = [t.Type<string>, t.InterfaceType<{ name: t.Type<string>; }>]
type B = typeof U.types

// type C = t.Type<string>
type C = typeof U.types[1]['props']['name']

@gcanti
Copy link
Owner

gcanti commented Jun 6, 2017

to build database schemas and stuff from the Type definition

@DylanRJohnston That's interesting, could you elaborate and/or show some practical example (in order to make sure the change above will be effective)?

@DylanRJohnston
Copy link
Author

DylanRJohnston commented Jun 6, 2017

I was thinking of something in the vein of the generic deriving from Haskell, so you could define a function that is able to compute a value from any Type representation. I'm trying to get it to generate a Sequelize schema object.

I think there's a problem with using the readonly _tag = StringLiteral when there's a Type that inherits from another type, e.g.

class Foo {
    readonly _tag = "Foo"
}

class Bar extends Foo {
    readonly _tag = "Bar"
}
Class 'Bar' incorrectly extends base class 'Foo'.
  Types of property '_tag' are incompatible.
    Type '"Bar"' is not assignable to type '"Foo"'.

@DylanRJohnston
Copy link
Author

It'd also be ideal to be able to case match on the different Type<primitive types> types too. There's a cool library that uses class decorators to achieve the same thing. But we were hoping to stick with structural typing via interfaces instead of nominal typing via classes. Might have to bite the bullet and just use classes.

@DylanRJohnston
Copy link
Author

I guess you could recover it by going

type ClassTags = "Foo" | "Bar"

interface HasTag {
    readonly _tag : ClassTags
}

class Foo {
    readonly _tag : ClassTags = "Foo"
}

class Bar extends Foo {
    readonly _tag = "Bar"
}

function isFoo(x : HasTag): x is Foo {
    return x._tag === "Foo";
}

function isBar(x : HasTag): x is Bar {
    return x._tag === "Bar";
}

const assertNever = (x: never): never => {throw new Error(x);}

function foobar(x : Foo | Bar) {
    if (isFoo(x)) {
        return ""
    } else if (isBar(x)) {
        return ""
    } else {
        return assertNever(x);
    }
}

gcanti added a commit that referenced this issue Jun 7, 2017
@gcanti
Copy link
Owner

gcanti commented Jun 7, 2017

@DylanRJohnston You can find a proof of concept in the 50 branch

  • But to leverage totality checking I need to be able to form some kind of tagged union of the Types in this package
  • It'd also be ideal to be able to case match on the different Type types too
  • a Type that inherits from another type (<= Q: does this still make sense? Now types are structural)

Example

import * as t from 'io-ts'

type Primitive = t.StringType | t.NumberType
type Primitives = { [key: string]: Primitive }
interface Schemas extends Array<Schema> {}
type Union = t.UnionType<Schemas, any>

type Schema = t.InterfaceType<Primitives> | Union

declare function f(type: Schema): string

const A = t.interface({
  a: t.string
})

const B = t.interface({
  b: t.number
})

const C = t.interface({
  c: t.boolean
})

const D = t.union([A, B])
const E = t.union([A, C])

f(A)
f(B)
f(C) // error: Type '"BooleanType"' is not assignable to type '"NumberType"'.
f(D)
f(E) // error

Could you please try it out with your use case? lib is committed in so you can install it by running

npm i gcanti/io-ts#50

@DylanRJohnston
Copy link
Author

Yes! Thank you so much. It works great.

@gcanti gcanti modified the milestone: 0.5 Jun 13, 2017
@gcanti gcanti closed this as completed in 07ad5e3 Jun 14, 2017
gcanti added a commit that referenced this issue Jun 14, 2017
Tagged Union of Types, closes #50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants