codedownio / aeson-typescript Goto Github PK
View Code? Open in Web Editor NEWGenerate TypeScript definition files from your ADTs
License: BSD 3-Clause "New" or "Revised" License
Generate TypeScript definition files from your ADTs
License: BSD 3-Clause "New" or "Revised" License
The example with type D
doesn't seem to work on the type alone (without given type params).
Similar behaviour (same error) happens with more simple example from tests:
data HigherKind a = HigherKind { higherKindList :: [a] }
$(deriveTypeScript A.defaultOptions ''HigherKind)
$(deriveJSON A.defaultOptions ''HigherKind)
main :: IO ()
main = putStrLn $ formatTSDeclarations $ getTypeScriptDeclarations (Proxy :: Proxy HigherKind)
which results in:
app/Main.hs:31:42: error:
• No instance for (TypeScript HigherKind)
arising from a use of ‘getTypeScriptDeclarations’
• In the second argument of ‘($)’, namely
‘getTypeScriptDeclarations (Proxy :: Proxy HigherKind)’
In the second argument of ‘($)’, namely
‘formatTSDeclarations
$ getTypeScriptDeclarations (Proxy :: Proxy HigherKind)’
In the expression:
putStrLn
$ formatTSDeclarations
$ getTypeScriptDeclarations (Proxy :: Proxy HigherKind)
|
31 | main = putStrLn $ formatTSDeclarations $ getTypeScriptDeclarations (Proxy :: Proxy HigherKind)i
This is a weird bug that I noticed and took time to track down in our code base 😄
In essence, you can reproduce it by modifying a bit the inputs of the test /Formatting/when given multiple Sum Types
.
E.g., https://github.com/codedownio/aeson-typescript/blob/master/test/Formatting.hs#L50
D2
type, similar to D
data D = S | F deriving (Eq, Show)
$(deriveTypeScript defaultOptions ''D)
data D2 = S2 | F2 deriving (Eq, Show)
$(deriveTypeScript defaultOptions ''D2)
Add D2
to the list of TSDeclaration
used in the 3 unit tests: replace (getTypeScriptDeclarations @D Proxy)
with (getTypeScriptDeclarations @D Proxy <> getTypeScriptDeclarations @D2 Proxy)
💥 Notice that the generated Typescript code gets completely wrong
E.g., for test should generate a TS Enum
, you would expect the output to be enum D { S, F }\n\nenum D2 { S2, F2 }
, right?
Well it turns out the actual output becomes type D = \"S\" | \"F\";\n\ntype D2 = \"S2\" | \"F2\";
, i.e. it behaves like typeAlternativesFormat = TypeAlias
!
The same bug happens for EnumWithType
.
I have a type representing form fields. The field values can be one of several types. Those values can be optional or required, depending on the endpoint. For example, the values are optional in the request to create the form and required in the request to submit the form.
Here is a simplified version of that type:
data FieldType f
= TextField (f Text)
| IntField (f Int)
type OptionalField = FieldType Maybe
type RequiredField = FieldType Identity
My intention was to generate TypeScript types like this:
type TOptionalField = IOptionalTextField | IOptionalIntField
interface IOptionalTextField {
tag: 'OptionalTextField'
value?: string
}
interface IOptionalIntField {
tag: 'OptionalIntField'
value?: number
}
type TRequiredField = IRequiredTextField | IRequiredIntField
interface IRequiredTextField {
tag: 'RequiredTextField'
value: string
}
interface IRequiredIntField {
tag: 'RequiredIntField'
value: number
}
But I can't do this because the TSDeclaration
constructors aren't exported. And a TypeScript
instance for this can't be derived because TypeScript doesn't have higher-order generics. Any thoughts on this? Writing the instance by hand is more error prone and probably not desirable in most cases. But without that ability, I am blocked from using a sensible Haskell type.
Building 0.5.0.0 with GHC 9.6 and cabal causes these errors:
src/Data/Aeson/TypeScript/Recursive.hs:64:3: error: [GHC-88464]
Variable not in scope:
forM_ :: [Name] -> (Name -> WriterT w Q ()) -> WriterT w Q a2
|
64 | forM_ names $ \n -> deriveInstanceIfNecessary n deriveFn
| ^^^^^
src/Data/Aeson/TypeScript/Recursive.hs:81:5: error: [GHC-88464]
Variable not in scope: when :: Bool -> m1 a3 -> MaybeT Q a4
|
81 | when (datatypeVars /= []) $ fail ""
| ^^^^
src/Data/Aeson/TypeScript/Recursive.hs:102:17: error: [GHC-88464]
Variable not in scope: unless :: Bool -> StateT [Name] Q () -> m b
|
102 | unless (n `L.elem` st) $ do
| ^^^^^^
src/Data/Aeson/TypeScript/Recursive.hs:106:11: error: [GHC-88464]
Variable not in scope:
forM_ :: [Type] -> (Type -> t2) -> StateT [Name] Q ()
|
106 | forM_ parentTypes $ \typ -> do
| ^^^^^
src/Data/Aeson/TypeScript/Recursive.hs:108:13: error: [GHC-88464]
Variable not in scope: forM_ :: [Name] -> (Name -> m0 b0) -> t2
|
108 | forM_ names maybeRecurse
| ^^^^^
src/Data/Aeson/TypeScript/Recursive.hs:119:7: error: [GHC-88464]
Variable not in scope:
unless
:: Bool
-> StateT ([Name], [Type]) Data.Functor.Identity.Identity ()
-> m a1
|
119 | unless (t1 `L.elem` visitedTypes) $ do
| ^^^^^^
src/Data/Aeson/TypeScript/Recursive.hs:124:7: error: [GHC-88464]
Variable not in scope:
unless
:: Bool
-> StateT ([Name], [Type]) Data.Functor.Identity.Identity () -> m b
|
124 | unless (t2 `L.elem` visitedTypes') $ do
| ^^^^^^
I'm a bit confused about how this happens, since it seems like 5ae6687 should fix this, and that commit is a parent of 5ef8e55, which is v0.5.0.0??? But somehow I'm seeing these errors on v0.5.0.0 and not on master. It'd be really handy to get a new version cut which compiles on GHC 9.6.
This code:
aeson-typescript/src/Data/Aeson/TypeScript/Instances.hs
Lines 114 to 116 in 16d6c2a
seems to assume aeson
generates maps for Data.Map
.
This does not appear to be the case. Since JavaScript maps must be keyed by strings, aeson
instead generates key-value pairs, see:
and the discussions at:
haskell/aeson#259
haskell/aeson#79
It seems aeson-typescript
should instead generate key-value pairs.
In cases where a sum type is transcribed as a series of interfaces and a union type:
export type U = A | B | C;
export interface A {
tag: "A";
contents: ...
}
export interface B {
tag: "B";
contents: ...
}
export interface C {
tag: "C";
contents: ...
}
Type predicates (https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates) would be useful:
function isA(u: A | B | C): U is A {
return (u as A).tag == "A";
}
Suppose we have this:
data MyThing' a = MyThing a | Nope Int
$(deriveToJSONAndTypeScript defaultOptions ''MyThing')
It'll generate code like this:
export type TMyThing'<T> = IMyThing<T> | INope<T>
type IMyThing<T> = {
tag: "MyThing"
contents: T
}
type INope<T> = {
tag: "Nope"
contents: number
}
TypeScript fails to parse this.
It'd be nice if the TH code threw an error about it, instead.
Thanks for aeson-typescript
, it's great!
I've noticed that when using Data.Map
as follows:
newtype Example = Example (Map String Int)
$(deriveTypeScript defaultOptions ''Foo)
result = tt @Example
(helper functions)
tsFormatOpts :: FormattingOptions
tsFormatOpts = defaultFormattingOptions {exportMode = ExportEach, typeAlternativesFormat = EnumWithType}
ts :: (TypeScript a) => Proxy a -> String
ts = formatTSDeclarations' tsFormatOpts . getTypeScriptDeclarations
tt :: forall a. (TypeScript a) => String
tt = ts (Proxy @a)
The generated types look like this:
export type Example = IExample;
// actual generated type
export type IExample = {[ k in string] ?: number};
It's a bit difficult to use these types, since indexing IExample
will give a number | undefined
value, even though the ToJSON instance for Data.Map
should never include undefined
values.
const foo: Example = { bar : undefined } // no error
function example(data: Example) {
const bar: { [k : string] : number } = data;
// Type 'IExample' is not assignable to type '{ [k: string]: number; }'.
// 'string' index signatures are incompatible.
// Type 'number | undefined' is not assignable to type 'number'.
// Type 'undefined' is not assignable to type 'number'.
}
Note that these errors still happen with exactOptionalPropertyTypes
enabled.
Instead, I would have expected the generated type to look more like this:
// desired type
export type IExample = { [k : string] : number };
const foo: Example = { bar : undefined }
// Type 'undefined' is not assignable to type 'number'.
function example(data: Example) {
const bar: { [k : string] : number } = data;
// Type 'IExample' is not assignable to type '{ [k: string]: number; }'.
// 'string' index signatures are incompatible.
// Type 'number | undefined' is not assignable to type 'number'.
// Type 'undefined' is not assignable to type 'number'.
}
String
or an alias for String
?Allow formatting sum types in different fashions.
data CountryCode = EC | US | MU deriving (Eq, Show)
The default and the way it currently works
type CountryCode = "EC" | "US" | "MU";
Create a plain TS enum:
enum CountryCode {
EC,
US,
MU,
}
Creates an enum with a type declaration:
enum CountryCodeEnum {
EC="EC",
US="US",
MU="MU"
}
type CountryCode = keyof typeof CountryCodeEnum;
This allows us to generate the types from the Enum
, which is the intended use in TS, and also allows us to generate collections from the Enum
using Object.keys(CountryCodeEnum)
.
There are some options like unwrapUnaryRecords
that aren't fully tested yet.
Idea: for every possible combination of Aeson options, use Template Haskell to generate a test file that derives instances using those options and checks everything with the tsc
compiler.
I believe it should be "string"
I'm curious if you'd be open to considering this change, I'd be curious to see how much a lift it'd be to work on a PR.
The blocker I have in using this to generate typescript is that in Haskell, all unions are disjoint, e.g.
data D a = A Int | B Int | C a
has no overlap between constructors A and B, even though the remainder of the types are the same.
If the generated typescript for this is
type D<T> = A<T> | B<T> | C<T>;
type A<T> = number;
type B<T> = number;
type C<T> = T
then there's no distinction between constructors A and B in the typescript code. The generally accepted pattern for this in typescript is to "tag" unions, essentially pairing each type of the union with a literal for it's constructor. So you'd have instead something like:
type D<T> = A<T> | B<T> | C<T>;
type A<T> = {tag: "A", value: number};
type B<T> = {tag: "B", value: number};
type C<T> = {tag: "C", value: T}
which produces a type that is actually equivalent to the Haskell type that generated it.
Thoughts?
I'm having difficulty deriving TypeScript instances for a higher-kinded, indirectly recursive type. I think this should work, given that Aeson happily generates JSON instances for the same type. This is with aeson-typescript
version 0.4.0.0. Here's a minimal reproduction:
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
module Test () where
import qualified Data.Aeson as Aeson
import Data.Functor.Identity
import GHC.Generics (Generic)
import qualified Data.Aeson.TypeScript.TH as TS
data Foo a = Foo (Identity (Foo a))
deriving (Generic)
$(TS.deriveTypeScript Aeson.defaultOptions ''Foo)
GHC gives me the following error:
Test.hs:20:3-48: error:
• Could not deduce (TS.TypeScript (Identity (Foo TS.T)))
arising from a use of ‘TS.getTypeScriptType’
from the context: (TS.TypeScript (Identity (Foo a)),
TS.TypeScript a)
bound by the instance declaration
at Test.hs:20:3-48
• In the expression:
TS.getTypeScriptType
(Data.Proxy.Proxy :: Data.Proxy.Proxy (Identity (Foo TS.T)))
In the third argument of ‘aeson-typescript-0.4.0.0:Data.Aeson.TypeScript.Types.TSTypeAlternatives’,
namely
‘[TS.getTypeScriptType
(Data.Proxy.Proxy :: Data.Proxy.Proxy (Identity (Foo TS.T)))]’
In the expression:
((aeson-typescript-0.4.0.0:Data.Aeson.TypeScript.Types.TSTypeAlternatives
"IFoo")
[(TS.getTypeScriptType (Data.Proxy.Proxy :: Data.Proxy.Proxy TS.T)
<> "")])
[TS.getTypeScriptType
(Data.Proxy.Proxy :: Data.Proxy.Proxy (Identity (Foo TS.T)))]
|
20 | $(TS.deriveTypeScript Aeson.defaultOptions ''Foo)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Test.hs:18:3-48: Splicing declarations
TS.deriveTypeScript Aeson.defaultOptions ''Foo
======>
instance (TS.TypeScript (Identity (Foo a_akjJ)),
TS.TypeScript (a_akjJ :: ghc-prim-0.6.1:GHC.Types.Type)) =>
TS.TypeScript (Foo (a_akjJ :: ghc-prim-0.6.1:GHC.Types.Type)) where
TS.getTypeScriptType _
= ("Foo"
<>
("<"
<>
((base-4.14.3.0:Data.OldList.intercalate ", ")
[TS.getTypeScriptType
(Data.Proxy.Proxy :: Data.Proxy.Proxy a_akjJ)]
<> ">")))
TS.getTypeScriptDeclarations _
= (((aeson-typescript-0.4.0.0:Data.Aeson.TypeScript.Types.TSTypeAlternatives
"Foo")
[(TS.getTypeScriptType (Data.Proxy.Proxy :: Data.Proxy.Proxy TS.T)
<> "")])
[("IFoo"
<>
(let
vars_aklh
= [(TS.getTypeScriptType
(Data.Proxy.Proxy :: Data.Proxy.Proxy TS.T)
<> "")]
in
("<"
<>
((base-4.14.3.0:Data.OldList.intercalate ", ") vars_aklh
<> ">"))))]
: [((aeson-typescript-0.4.0.0:Data.Aeson.TypeScript.Types.TSTypeAlternatives
"IFoo")
[(TS.getTypeScriptType (Data.Proxy.Proxy :: Data.Proxy.Proxy TS.T)
<> "")])
[TS.getTypeScriptType
(Data.Proxy.Proxy :: Data.Proxy.Proxy (Identity (Foo TS.T)))]])
TS.getParentTypes _
= [TS.TSType
(Data.Proxy.Proxy :: Data.Proxy.Proxy (Identity (Foo a_akjJ)))]
The problem doesn't occur if you leave out the Identity
. Replacing Identity
with []
yields only a warning (this may have to do with the fact that []
has an instance in Data.Aeson.TypeScript.Instances
).
I currently don't see how to use this library with custom JSON instances, especially since the constructors for TSDeclaration
are not exposed and I don't see why.
Imagine a type and the following JSON instance, which sort of "flattens":
data Foo = Foo { a :: String, b :: SomeOtherProductType }
instance ToJSON Foo where
toJSON (Foo a b) = case toJSON b of
(Object hm) -> Object $ HM.insert "a" (toJSON a) hm
_ -> error "oops"
How would you write an instance for this without messing with the TypeScript instance of SomeOtherProductType
? I don't see a reasonable way.
In some cases it'd be really useful to include Haddock documentation in generated types. As of TemplateHaskell 2.18 (GHC 9.2), there's a getDoc
function which allows you to obtain Haddock docs inside the Q
monad for a given Name
, which means that we can call it inside deriveTypeScript
. This functionality has been implemented in the Moat library (which generates Kotlin and Swift types from Haskell type definitions in a similar way; see MercuryTechnologies/moat#56) already, so I think it ought to be possible here too.
I think we'd want to make this configurable, since it's possible that some people have setups where the Haddock documentation contains information that they wouldn't want to expose to whoever is consuming the TypeScript types.
I'd suggest going about it as follows:
generateDocComments :: Bool
field to ExtraTypeScriptOptions
which determines whether to attempt to pull doc comments when generating TypeScript types.Maybe
fields to the appropriate TypeScript AST types - TSDeclaration
etc - to hold doc comments. We'd set these fields to Nothing
if generateDocComments
is set to False
, if TemplateHaskell is not at least at version 2.18, or if the type isn't documented.formatTSDeclaration
etc to render those doc comments alongside the type if present.Does this sound like something you'd be interested in merging if I were to send a PR?
[7 of 9] Compiling Data.Aeson.TypeScript.TH ( src/Data/Aeson/TypeScript/TH.hs, /tmp/aeson-typescript-0.3.0.0/dist-newstyle/build/x86_64-linux/ghc-9.0.1/aeson-typescript-0.3.0.0/build/Data/Aeson/TypeScript/TH.o, /tmp/aeson-typescript-0.3.0.0/dist-newstyle/build/x86_64-linux/ghc-9.0.1/aeson-typescript-0.3.0.0/build/Data/Aeson/TypeScript/TH.dyn_o )
src/Data/Aeson/TypeScript/TH.hs:315:37: error:
• Couldn't match expected type: TyVarBndr ()
with actual type: flag0 -> TyVarBndr flag0
• Probable cause: ‘PlainTV’ is applied to too few arguments
In the expression: PlainTV f
In the third argument of ‘DataD’, namely ‘[PlainTV f]’
In the expression: DataD [] name' [PlainTV f] Nothing [] []
|
315 | let inst1 = DataD [] name' [PlainTV f] Nothing [] []
| ^^^^^^^^^
FAIL (5.98s)
ErrorCall (TSC check failed:
CallStack (from HasCallStack):
error, called at test/Util.hs:79:5 in main:Util)
TaggedObject with tagSingleConstructors=True
type checks everything with tsc: FAIL (5.34s)
ErrorCall (TSC check failed:
CallStack (from HasCallStack):
error, called at test/Util.hs:79:5 in main:Util)
TaggedObject with tagSingleConstructors=False
type checks everything with tsc: FAIL (4.06s)
ErrorCall (TSC check failed:
CallStack (from HasCallStack):
error, called at test/Util.hs:79:5 in main:Util)
TwoElemArray with tagSingleConstructors=True
type checks everything with tsc: FAIL
ErrorCall (TSC check failed:
CallStack (from HasCallStack):
error, called at test/Util.hs:79:5 in main:Util)
TwoElemArray with tagSingleConstructors=False
type checks everything with tsc: FAIL
ErrorCall (TSC check failed:
CallStack (from HasCallStack):
error, called at test/Util.hs:79:5 in main:Util)
UntaggedValue
type checks everything with tsc: FAIL
ErrorCall (TSC check failed:
CallStack (from HasCallStack):
error, called at test/Util.hs:79:5 in main:Util)
Higher kinds
Kind * -> *
makes the declaration and types correctly: OK
works when referenced in another type: OK
works with an interface inside: OK
Kind * -> * -> *
makes the declaration and type correctly: OK
TSC compiler checks
type checks everything with tsc: FAIL
ErrorCall (TSC check failed:
CallStack (from HasCallStack):
error, called at test/Util.hs:79:5 in main:Util)
8 out of 12 tests failed (5.98s)
interfaceNameModifier
is documented as "Function applied to generate interface names" and so I'd expect it to apply to the whole interface name, but the call to it for TSInterfaceDeclaration
splits off the first character and applies the function to the tail:
modifiedInterfaceName = (\(li, name) -> li <> interfaceNameModifier name) . splitAt 1 $ interfaceName
this is especially awkward since 0.5.0.0 when the default formatting options were changed such that names are validated as valid typescript identifiers; that validation expects the full identifier, and so fails if the second character isn't a valid first identifier character:
import Data.Aeson.TypeScript.TH
import Data.Aeson.TypeScript.Internal
let decl = TSInterfaceDeclaration "A1" [] [] Nothing
formatTSDeclaration defaultFormattingOptions decl
⇒
"interface A*** Exception: The name 1 contains illegal characters: 1
Consider setting a default name formatter that replaces these characters, or renaming the type.
CallStack (from HasCallStack):
error, called at src/Data/Aeson/TypeScript/Types.hs:157:11 in aeson-typescript-0.6.0.0-HYYVUss2s6t2E3z9zSEOcD:Data.Aeson.TypeScript.Types
(https://github.com/codedownio/aeson-typescript/blob/v0.6.3.0/src/Data/Aeson/TypeScript/LegalName.hs#L12-L39)
(#35)
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.