Good question! The Pro tier includes a scaffolding tool for exactly this purpose. It gets you started with an Encoder and Decoder based on a TypeScript type as input. I’m still working on getting some video tutorials and demos to showcase it better, but for now here’s a small gif demo (this is live in production for pro users).
And here’s the full resulting generated code from that input type:
type User
= Admin { adminId : Float }
| Guest
| Regular { first : String, last : String }
userEncoder : Encoder User
userEncoder =
Encode.union
(\vAdmin vGuest vRegular value ->
case value of
Admin data ->
vAdmin data
Guest ->
vGuest
Regular data ->
vRegular data
)
|> Encode.variant
(Encode.object
[ Encode.required "role" identity (Encode.literal <| JE.string "admin")
, Encode.required "adminId" .adminId Encode.float
]
)
|> Encode.variantLiteral (JE.object [ ( "role", JE.string "guest" ) ])
|> Encode.variant
(Encode.object
[ Encode.required "role" identity (Encode.literal <| JE.string "regular")
, Encode.required "first" .first Encode.string
, Encode.required "last" .last Encode.string
]
)
|> Encode.buildUnion
userDecoder : Decoder User
userDecoder =
Decode.oneOf
[ Decode.succeed (\() adminId -> Admin { adminId = adminId })
|> Decode.andMap (Decode.field "role" (Decode.literal () (JE.string "admin")))
|> Decode.andMap (Decode.field "adminId" Decode.float)
, Decode.literal Guest (JE.object [ ( "role", JE.string "guest" ) ])
, Decode.succeed (\() first last -> Regular { first = first, last = last })
|> Decode.andMap (Decode.field "role" (Decode.literal () (JE.string "regular")))
|> Decode.andMap (Decode.field "first" Decode.string)
|> Decode.andMap (Decode.field "last" Decode.string)
]
The really nice thing about the scaffolding tool is that it will start you out with an Encoder or Decoder that yields or accepts that TypeScript type. But from there, you take the code and own it, and as you make changes the changed types are reflected in the TypeScript and Elm types, so it’s always in sync.