m-bock/purescript-ts-bridge

How to export Variant type

Closed this issue · 5 comments

Hi there,

I'm following the FAQ advice to convert a sum type to a variant to remove it's opacity however, I'm missing the last step which is the ability to import the Variant type by name into my typescript code.

Once I've defined the type:

data Some
  = Root 
  | Segments (Array String)

type SomeV = Variant 
  ( root :: {} 
  , segments :: { _1 :: Array String }
  )

And a value of that type (converting the sum type with labeled-data:

someV :: TaskPathV
someV = genericToVariant (Proxy :: _ (LowerFirst /\ ArgsToRecord (Prefix "_"))) someValue

I do get a correct type for someV:

export const someV : ({ readonly 'type': 'segments'; readonly 'value': { readonly '_1': Array<string>; }; }) | ({ readonly 'type': 'root'; readonly 'value': {}; })

But I can't figure out how I could generate instead:

export type SomeV = { readonly 'type': 'segments'; readonly 'value': { readonly '_1': Array<string>; }; }) | ({ readonly 'type': 'root'; readonly 'value': {}; }
export const someV : import('../Module').SomeV

So that I can import the Variant type by name.

An additional question: the @typescript-eslint/ban-types rule doesn't like the {} type, is that expected or is there a better way to encode nullary constructors as Variants with labeled-data ?

Don't use `{}` as a type. `{}` actually means "any non-nullish value".
- If you want a type meaning "any object", you probably want `object` instead.
- If you want a type meaning "any value", you probably want `unknown` instead.
- If you want a type meaning "empty object", you probably want `Record<string, never>` instead.
- If you really want a type meaning "any non-nullish value", you probably want `NonNullable<unknown>` instead.eslint[@typescript-eslint/ban-types](https://typescript-eslint.io/rules/ban-types)

I can work around it with an index.d.ts

import { someV } from "./output/Module/index";
export type SomeV = typeof someV;

but it would be nice to have a cleaner solution.

m-bock commented

Hi!
This is intended behavior for non-nominal types. If you want the behavior that you describe, you should create an TsBridge instance for your type. I suggest to have a look how it's done for Either:
https://github.com/thought2/purescript-ts-bridge/blob/main/src/TsBridge/Class.purs#L28 (you have to follow from there to another module to see the implementation)
Then, all values of your type should reference to the this type on the TS side.

Let me know, if you need more information or a more complete code sample.

Regarding the {} that's a good, point. I'll research about this.

m-bock commented

Wait, Either is not implemented with labeled-data. I suggest it's better to use tsBridgeNewtype and combine it with what you already have. I'll add better documentation for custom ADTs.

m-bock commented

Yesterday I was in a hurry, I may have caused confusion. So here's a complete example for your case. The example is also added to the Demo repo

Define the variant type like this:

newtype SomeV = SomeV
  ( Variant
      ( root :: {}
      , segments :: { _1 :: Array String }
      )
  )

Give it a newtype instace:

derive instance Newtype SomeV _

Now you can write an instance for TsBridge like so:

instance TsBridge SomeV where
  tsBridge = TSB.tsBridgeNewtype Tok
    { moduleName
    , typeName: "SomeV"
    , typeArgs: []
    }

You can create values of the Newtype wrapped Variant as usual:

val1 :: SomeV
val1 = SomeV $ V.inj (Proxy :: _ "root") {}

val2 :: SomeV
val2 = SomeV $ V.inj (Proxy :: _ "segments") { _1: [ "a", "b" ] }

If you export val1 and val2 with ts-brige, you'll get this TS code:

export type SomeV =
  | { readonly type: "root"; readonly value: {} }
  | {
      readonly type: "segments";
      readonly value: { readonly _1: Array<string> };
    };

export const val1: import("../SampleApp.Lib").SomeV;

export const val2: import("../SampleApp.Lib").SomeV;

But most likely you want to work with the ADT version of your type internally, make sure you have a Generic instance:

data Some
  = Root
  | Segments (Array String)

derive instance Generic Some _

All we need now are functions that convert between the ADT and the Variant. You can write them manually, or use the labeled-data library to avoid boilerplate:

type DefaultOpts = (LowerFirst /\ ArgsToRecord (Prefix "_"))

someToSomeV :: Some -> SomeV
someToSomeV = SomeV <<< genericToVariant (Proxy :: _ DefaultOpts)

someVToSome :: SomeV -> Some
someVToSome = un SomeV >>> genericFromVariant (Proxy :: _ DefaultOpts)

That's it. Hope it's helpful.

Understanding how to useTSB.tsBridgeNewtype for this was exactly what I needed, thank you so much for also adding it to the Demo 🙏