TypeScript Support - atdts

This documentation is incomplete. Your help would be appreciated! In particular, some how-to guides would be great.

Tutorials

Hello World

Install atdts with opam:

opam install atdts

Create a file hello.atd containing this:

type message = {
  subject: string;
  body: string;
}

Call atdts to produce hello.ts:

$ atdts hello.atd

There’s now a file hello.ts that contains a class looking like this:

...

export type Message = {
  subject: string;
  body: string;
}

export function writeMessage(x: Message, context: any = x): any {
  ...
}

export function readMessage(x: any, context: any = x): Message {
  ...
}

...

Let’s write a TypeScript program say_hello.ts that uses this code:

import * as hello from "./hello"

const msg: hello.Message = {
  subject: "Hello",
  body: "Dear friend, I hope you are well."
}

console.log(JSON.stringify(hello.writeMessage(msg)))

Running it will print the JSON message:

$ tsc --lib es2017,dom say_hello.ts
{"subject":"Hello","body":"Dear friend, I hope you are well."}

Such JSON data can be parsed. Let’s write a program read_message.ts that consumes JSON data from standard input:

import * as hello from "./hello"
import * as readline from "readline"

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
})

rl.question('', (data: string) => {
  const msg = hello.readMessage(JSON.parse(data))
  console.log("subject: " + msg.subject)
})

Output:

# Install dependencies
$ npm install --save-dev @types/node
$ npm install readline

# Compile
$ tsc --lib es2017,dom read_message.ts

# Run
$ echo '{"subject": "big news", "body": ""}' | js read_message.js
subject: big news

It works! But what happens if the JSON data lacks a "subject" field? Let’s see:

$ echo '{"body": ""}' | js read_message.js
{"body": ""}
readline.js:1086
            throw err;
            ^

Error: missing field 'subject' in JSON object of type 'Message'
...

And what if our program also thought that the correct field name was subj rather than subject? Here’s read_message_wrong.ts which tries to access a subj field:

import * as hello from "./hello"
import * as readline from "readline"

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
})

rl.question('', (data: string) => {
  const msg = hello.readMessage(JSON.parse(data))
  console.log("subject: " + msg.subj)
})

Let’s compile our program:

$ tsc --lib es2017,dom read_message_wrong.ts
read_message_wrong.ts:11:33 - error TS2339: Property 'subj' does not exist on type 'Message'.

11   console.log("subject: " + msg.subj)
                                   ~~~~


Found 1 error in read_message_wrong.ts:11

The typechecker detected that our program makes incorrect assumptions about the message format without running it.

ATD Records, JSON objects, TypeScript objects

An ATD file contains types that describe the structure of JSON data. JSON objects map to TypeScript types and objects. They’re called records in the ATD language. Let’s define a simple record type in the file hello_plus.atd:

type message = {
  subject: string;
  ~body: string;
}

Note the ~ in front of the body field. It means that this field has a default value. Whenever the JSON field is missing from a JSON object, a default value is assumed. The implicit default value for a string is "".

Let’s add a signature field whose default value isn’t the empty string:

type message = {
  subject: string;
  ~body: string;
  ~signature <ts default="'anonymous'">: string;
}

Finally, we’ll add an optional url field that doesn’t take a default value at all:

type message = {
  subject: string;
  ~body: string;
  ~signature <ts default="'anonymous'">: string;
  ?url: string option;
}

Let’s generate the TypeScript code for this.

$ atdts hello_plus.atd

Let’s update our reader program read_message_plus.ts to this:

import * as hello_plus from "./hello_plus"
import * as readline from "readline"

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
})

rl.question('', (data: string) => {
  const msg = hello_plus.readMessage(JSON.parse(data))
  console.log(msg)
})

We can test it, showing us the final value of each field:

$ tsc --lib es2017,dom read_message_plus.ts
$ echo '{"subject":"hi"}' | js read_message_plus.js
{"subject":"hi"}
{ subject: 'hi',
  body: '',
  signature: 'anonymous',
  url: undefined }

How-to guides

Defining default field values

[missing]

Renaming field names

[missing]

Deep dives

[missing]

Reference

Type mapping

ATD type

TypeScript type

JSON example

unit

null

null

bool

bool

True

int

Int*

42 or 42.0

float

number

6.28

string

string

"Hello"

string list

string[]

["a", "b", "c!"]

(bool * float)

[boolean, number]

[-1, 1]

int nullable

Int | null

42 or null

abstract

any

anything

{ id: string }

{ id: string }

{"id": "3hj8d"}

[A | B of int]

{kind: 'A'} | {kind: 'B', value: Int}

"A" or ["B", 5]

foo_bar

FooBar

*the Int type is an alias for number but additionally, the read and write functions generated by atdts check that the number is a whole number.

Supported ATD annotations

Default field values

Record fields following a ~ assume a default value. The default value can be implicit as mandated by the ATD language specification (false for bool, zero for int, etc.) or it can be a user-provided value.

A user-provided default uses an annotation of the form <ts default="VALUE"> where VALUE evaluates to a TypeScript expression e.g.

type foo = {
  ~answer <ts default="42">: int;
}

For example, the JSON value {} will be read as {answer: 42}.

Field and constructor renaming

Alternate JSON object field names can be specified using an annotation of the form <json name="NAME"> where NAME is the desired field name to be used in the JSON representation. For example, the following specifies the JSON name of the id field is ID:

type foo = {
  id <json name="ID">: string
}

Similarly, the constructor names of sum types can also be given alternate names in the JSON representation. Here’s an example:

type bar = [
| Alpha <json name="alpha">
| Beta <json name="beta"> of int
]

Import declarations

ATD import statements allow types defined in other ATD files to be referenced from a given ATD file. For example:

from ext_types import tag, status
from long.module.path as ext import tag

For an import of the form from ext_types import tag, atdts generates a TypeScript import statement:

import * as ext_types from "./ext_types"

Types from that module are then referenced as ext_types.Tag, and the accompanying read/write functions as ext_types.readTag and ext_types.writeTag.

The <ts name="NAME"> annotation on the module path overrides the local TypeScript module name used in the import and in type references. For example:

from ext_types <ts name="etypes"> import tag

generates:

import * as etypes from "./ext_types"

When an alias is given, the alias name is used as the local TypeScript module name. The <ts name="NAME"> annotation on the path still controls the generated import name if present. For example:

from long.module.path as ext import tag

generates:

import * as ext from "./long/module/path"

Note that dotted module paths are mapped to file paths using / as the separator (e.g. long.module.path becomes "./long/module/path").

Note: The <ts from="..."> annotation on individual type definitions is an older mechanism for referencing types from other modules. The from ... import statement is the preferred approach for multi-file ATD projects.

Field from

Position: left-hand side of a type definition, after the type name

Values: .ts file with exported types. This can be also seen as the name of the original ATD file, without the .atd extension and capitalized like an ATD module name.

Semantics: specifies the base name of the ATD modules where the type and values coming with that type are defined.

Example: First input file part1.atd:

type point = { x : int; y : int }

Second input file part2.atd depending on the first one:

type point <ts from="Part1"> = abstract
type points = point list

To use a different type name than defined in the Part1 module, add a t field declaration to the annotation which refers to the original type name:

type point_xy <ts from="Part1" t="point"> = abstract
type points = point_xy list

Alternate representations for association lists

List of pairs can be represented by JSON objects or by TypeScript maps if the correct annotations are provided:

  • (string * bar) list <json repr="object"> will use JSON objects to represent a list of pairs of TypeScript type [string, Bar][]. Using the annotation <json repr="array"> is equivalent to the default.

  • (foo * bar) list <ts repr="map"> will use a TypeScript map of type Map<Foo, Bar> to represent the association list. Using the annotation <ts repr="array"> is equivalent to the default.

Caveats

  • Generated typescript contains a flag telling the compiler not to run checks on the file. (Read)