How to define Frontend/Backend Contract with Elixir/TypeScript

I recently joined a team that uses Elixir, React, and TypeScript. I’m focused on the frontend bit, but there’s a mismatch between the TypeScript definitions and the API response types.

Are there ways to generate TypeScript interfaces in Elixir so we can maintain a single source of truth?

For example, In GraphQL, I’m used to using Relay, apollo codegen, and graphql-codegen to generate proper TypeScript interfaces from the GraphQL endpoint.

This library, openapi-typescript, also allows you to generate Typescript Interfaces in NodeJS.

I will appreciate help on how people handle this issue or possible solutions. Thanks!

As far as I know, you can’t have Typescript working with Elixir (if I understand correctly this is what you are somewhat looking for).

You do have dialyzer for Elixir, but since Elixir only has strong typing (not static typing) dialyzer offers less guarantees than Typescript. Typescript is more intrusive and therefore can give you more guarantees. You can find our more about dializer here:

https://learnyousomeerlang.com/dialyzer#success-typing

Then there is the other part, in that Elixir has certain types that dont even exist in JS, like atoms and tuples for example.

At the current time, I am unaware of any library that generates a valid Typescript document for Elixir apps.

Anyone feel free to correct me if I am wrong.

1 Like

I would say it’s possible, but have never tried… Absinthe is able to export schema to js.

Anyway I would try to use phoenix_swagger or open_api_spex and then openapi-typescript.

1 Like

I haven’t done this but you should be able to create schema.graphql file with mix absinthe.schema.sdl — absinthe v1.6.4 and then use graphql-codegen to create TypeScript definitions from it.

1 Like

What do you want to generate types from?

For instance, if you would generate types froms @type attributes, you would need to parse the types declaration.

Types in the String module are declared like this:

  @type t :: binary
  @type codepoint :: t
  @type grapheme :: t
  @type pattern :: t | [t] | :binary.cp

You can extract those with this code:

String
|> :code.which()
|> :beam_lib.chunks([:abstract_code])
|> elem(1)
|> elem(1)
|> Keyword.get(:abstract_code)
|> elem(1)
|> Enum.filter(fn
  {:attribute, _, :type, _} -> true
  _ -> false
end)

And you get that:

[
  {:attribute, 287, :type,
   {:pattern,
    {:type, 287, :union,
     [
       {:user_type, 287, :t, []},
       {:type, 0, :list, [{:user_type, 287, :t, []}]},
       {:remote_type, 287, [{:atom, 0, :binary}, {:atom, 0, :cp}, []]}
     ]}, []}},
  {:attribute, 284, :type, {:grapheme, {:user_type, 284, :t, []}, []}},
  {:attribute, 281, :type, {:codepoint, {:user_type, 281, :t, []}, []}},
  {:attribute, 278, :type, {:t, {:type, 278, :binary, []}, []}}
]

Now you can generate typescript types from such data.

It seems that you liked my answer so it probably worked. Can you mark it as solution so other people know?

Have a look at AsyncAPI which is (obviously) better suited for an async API than OpenAPI.

Here is a good overview if you are familiar with swagger: Coming from OpenAPI | AsyncAPI Initiative for event-driven APIs

EDIT: just learned that there is an Apache Avro implementation for Elixir: GitHub - klarna/erlavro: Avro support for Erlang/Elixir (http://avro.apache.org/)
AsyncAPI can use Avro as a schema language.

2 Likes

I’ve also successfully used https://www.graphql-code-generator.com/ with Absinthe. It works well for most simple queries and mutations.

The only gotcha is if you want to handle file uploads (in-line with this guide) you will need to extend the default “fetcher” client to send file uploads as multipart/form-data.

Hi,

I did not understand this point. We are also using typescript in one of our products and the backend is in elixir. Can you give me more insight on this? thanks :slight_smile:

Hello @rowlandekemezie ! Welcome to the Elixir Forum!

We are doing something like this in our campaign tool Proca.

  • The server exposes GraphQL API using Absinthe
  • We use GraphQL Code Generator to generate typed queries and mutations for each API consumer.

The types cover all the queries, mutations and subscriptions (what are arguments types, what are result types), as well as fragment types.
It works, and if you make a breaking change on the server side of things, you will get a compilation error from Typescript on the client side :slight_smile:

However, this setup turned out to come with some complexity. Here are the details:

Elixir Server:

Client libraries in Typescript, using GraphQL Codegen and Urql GraphQL client library:

  • The common code for a TS client package is placed in @proca/api package here: proca-server/sdk/api at main · fixthestatusquo/proca-server · GitHub - The common code is:

    1. src/apiTypes.ts - the top level types generated using codegen (see codegen.yml for configuration)
    2. Code that configures the client (sets the auth, api/websocket paths and so on)
  • The client app package - for example see the CLI package (@proca/cli) here: proca-server/sdk/cli at main · fixthestatusquo/proca-server · GitHub

    1. It also generates src/proca.ts file using GraphQL, but a specialized one; based on fragments defined to this particular projects (*.graphql files under src/). I really encourage to do this: write fragments to get types which are easier to manipulate (because they do not have all the associations, just core fields of interest), and to use specific query for each view or command. This way you will not have to use horrible Pick<...> declarations, the codegen will give you most straightforward types. This way you could probably not even need generic types from the @proca/api.
  • Codegen options: Codegen lets you set many options which affect the Typescript generated from .graphql files. Things like, are Tune to your tastes. I think what we use now is working best for us, and required some trial and error :slight_smile:

  • Bonus points: GraphQL supports custom scalar types; for example, we have an escape hatch where you can pass or get a JSON argument/field.
    On Absinthe we can define custom scalar serializer/deserializer and so Absinthe automatically will parse the JSON and pass a map to resolver. You might also want to define DateTime or Date types in similar way- they are passed as strings in graphql, but you get a natural developer experience.

    However, the TS graphql package does not allow anything like this; I am not sure why is that, perhaps because world of javascrpt is flaky.

    We have created an experimental Urql middleware (they call them “exchanges”) to serialize and deserialize custom scalars based on the knowledge of their location within the graphql schema (eg. We know that Person has a field metadata which is Json, so whenever we get a Person or list of Persons in a result, we can parse the JSON to return a JS object). The knowledge about scalar locations are generated by an additional codegen plugin, and stored along the operation/type definitions.

    Beware, this is experimental and used only by us so far; and there are some caveats when using with more complex types like interfaces.

2 Likes

Our company uses DeepPartial<T> so we don’t need to use fragments.

And our codegen.yml looks bit like this

src/schema-codegen.ts:
    config:
      defaultMapper: DeepPartial<{T}>
    ...
    plugins:
      - "typescript"
      - "typescript-resolvers"
      - add:
          content: "import { DeepPartial } from './common/partial';"

DeepPartial implementation

export type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends string | boolean | number | symbol
    ? T[K]
    : DeepPartial<T[K]>;
};

export function unwrapDeepPartial<T>(value: DeepPartial<T>): T {
  return value as T;
}