Giter VIP home page Giter VIP logo

aeson-typescript's Introduction

Welcome to aeson-typescript Hackage aeson-typescript

This library provides a way to generate TypeScript .d.ts files that match your existing Aeson ToJSON instances. If you already use Aeson's Template Haskell support to derive your instances, then deriving TypeScript is as simple as

$(deriveTypeScript myAesonOptions ''MyType)

For example,

data D a = Nullary
         | Unary Int
         | Product String Char a
         | Record { testOne   :: Double
                  , testTwo   :: Bool
                  -- | This docstring will go into the generated TypeScript!
                  , testThree :: D a
                  } deriving Eq

Next we derive the necessary instances.

$(deriveTypeScript (defaultOptions {fieldLabelModifier = drop 4, constructorTagModifier = map toLower}) ''D)

Now we can use the newly created instances.

>>> putStrLn $ formatTSDeclarations $ getTypeScriptDeclarations (Proxy :: Proxy (D T))

type D<T> = "nullary" | IUnary<T> | IProduct<T> | IRecord<T>;

type IUnary<T> = number;

type IProduct<T> = [string, string, T];

interface IRecord<T> {
  tag: "record";
  One: number;
  Two: boolean;
  // This docstring will go into the generated TypeScript!
  Three: D<T>;
}

It's important to make sure your JSON and TypeScript are being derived with the same options. For this reason, we include the convenience HasJSONOptions typeclass, which lets you write the options only once, like this:

instance HasJSONOptions MyType where getJSONOptions _ = (defaultOptions {fieldLabelModifier = drop 4})

$(deriveJSON (getJSONOptions (Proxy :: Proxy MyType)) ''MyType)
$(deriveTypeScript (getJSONOptions (Proxy :: Proxy MyType)) ''MyType)

Or, if you want to be even more concise and don't mind defining the instances in the same file,

myOptions = defaultOptions {fieldLabelModifier = drop 4}

$(deriveJSONAndTypeScript myOptions ''MyType)

Remembering that the Template Haskell Q monad is an ordinary monad, you can derive instances for several types at once like this:

$(mconcat <$> traverse (deriveJSONAndTypeScript myOptions) [''MyType1, ''MyType2, ''MyType3])

Suggestions for use

This library was written to make it easier to typecheck your TypeScript frontend against your Haskell backend. Here's how I like to integrate it into my workflow:

The idea is to set up a separate Haskell executable in your Cabal file whose sole purpose is to generate types. For example, in your hpack package.yaml file add a new executable like this:

executables:
  ...
  tsdef:
    main: Main.hs
    source-dirs: tsdef
    dependencies:
    - my-main-app
    ...

And tsdef/Main.hs should look like this:

module Main where

import Data.Proxy
import Data.Monoid
import MyLibraries

$(deriveTypeScript (getJSONOptions (Proxy :: Proxy MyType1)) ''MyType1)
$(deriveTypeScript (getJSONOptions (Proxy :: Proxy MyType2)) ''MyType2)
...

main = putStrLn $ formatTSDeclarations (
  (getTypeScriptDeclaration (Proxy :: Proxy MyType1)) <>
  (getTypeScriptDeclaration (Proxy :: Proxy MyType2)) <>
  ...
)

Now you can generate the types by running stack runhaskell tsdef/Main.hs > types.d.ts. I like to make this an automatic step in my Gulpfile, Webpack config, etc.

See also

If you want a more opinionated web framework for generating APIs, check out servant. If you use Servant, you may enjoy servant-typescript, which is based on aeson-typescript! This companion package also has the advantage of magically collecting all the types used in your API, so you don't have to list them out manually.

For another very powerful framework that can generate TypeScript client code based on an API specification, see Swagger/OpenAPI.

aeson-typescript's People

Contributors

9999years avatar bitonic avatar gelisam avatar ghais avatar jjkv avatar langston-barrett avatar nh2 avatar parsonsmatt avatar reisen avatar rimmington avatar tanyabouman avatar thomasjm avatar yuras avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

aeson-typescript's Issues

Stackage nightly test failure

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)

Always generate "tagged" unions

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?

[RFC] - Format Sum types

Allow formatting sum types in different fashions.

data CountryCode = EC | US | MU deriving (Eq, Show)

String Literal Types

The default and the way it currently works

type CountryCode = "EC" | "US" | "MU";

Enum

Create a plain TS enum:

enum CountryCode {
    EC,
    US,
    MU,
}

EnumWithType

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).

Include Haddock documentation in generated types

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:

  • Add a generateDocComments :: Bool field to ExtraTypeScriptOptions which determines whether to attempt to pull doc comments when generating TypeScript types.
  • Add 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.
  • Alter 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?

Extend Map instance to handle `ToJSONKeyValue` keys

This code:

instance (TypeScript a, TypeScript b) => TypeScript (Map a b) where
getTypeScriptType _ = "{[k in " ++ getTypeScriptKeyType (Proxy :: Proxy a) ++ "]?: " ++ getTypeScriptType (Proxy :: Proxy b) ++ "}"
getParentTypes _ = [TSType (Proxy :: Proxy a), TSType (Proxy :: Proxy b)]

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:

https://github.com/haskell/aeson/blob/65fca856304c0b323d538cc783493d8d7a145114/src/Data/Aeson/Types/ToJSON.hs#L1684

and the discussions at:

haskell/aeson#259
haskell/aeson#79

It seems aeson-typescript should instead generate key-value pairs.

Writing custom 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.

Cut a new version to fix GHC 9.6 compilation errors

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.

Fails to build with GHC 9.0.1

[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 [] []
    |                                     ^^^^^^^^^

Deriving TypeScript fails for a higher-kinded, indirectly-recursive type

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)
   |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Splices generated with `-ddump-splices`
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).

`interfaceNameModifier` applied to tail of interface name not whole interface name, triggers name format error for valid interface names

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

(https://github.com/codedownio/aeson-typescript/blob/v0.6.3.0/src/Data/Aeson/TypeScript/Formatting.hs#L60)

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)

Haskell types with a `'` in the name result in broken code

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.

Example doesn't seem to work

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

[Bug] `formatTSDeclarations'` does not honor `typeAlternativesFormat` if there is more than 1 `TSDeclaration` in the input list

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

  1. Add a new 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)
  1. Add D2 to the list of TSDeclaration used in the 3 unit tests: replace (getTypeScriptDeclarations @D Proxy) with (getTypeScriptDeclarations @D Proxy <> getTypeScriptDeclarations @D2 Proxy)

  2. 💥 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.

Handwritten TypeScript instances for higher-order ADTs

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.

Prohibit `undefined` in TypeScript Index Signatures for `Data.Map` when key is `String`

Thanks for aeson-typescript, it's great!

Example

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.

Desired Behavior

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'.
}

Questions

  • was there a particular reason for this choice of types?
  • would it be possible to change the behavior when the key is String or an alias for String?

Related

microsoft/TypeScript#46969

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.