14

Bringing TypeScript types at runtime with TypeOnly

 4 years ago
source link: https://www.tuicool.com/articles/RfMzuyn
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

With a guy on my team, we recently released a preliminary version of a side-project that could be promising. I wrote this article to present our work. And I start with: Why did we decide to create TypeOnly?

Because it’s not always possible to write DRY code with TypeScript…

TypeScript typing definitions are not available at runtime. Sometime this forces us to repeat ourselves, as in the following example:

type ColorName = "red" | "green" | "blue"
function isColorName(name: string): name is ColorName {
  return ["red", "green", "blue"].includes(name)
}

This kind of code is not ideal. There is an open discussion on Github related to this subject, and the TypeScript team is not ready to provide a solution.

A new language?

TypeOnly is a new language but not a new syntax. TypeOnly aims to be and remain a strict subset of TypeScript: any code that compiles with TypeOnly will also compile with TypeScript. It is the “pure typing” part of TypeScript: only interface and type definitions.

See also: A detailed description of the TypeOnly language .

The TypeOnly parser is implemented from scratch and does not require TypeScript as a dependency. It can be used outside a TypeScript project, such as in a JavaScript project, or to check JSON data with a command line tool.

How to check JSON data with TypeOnly (command line version)

Create a file “drawing.d.ts” with the following code:

// drawing.d.ts
export interface Drawing {
  color: ColorName
  dashed?: boolean
  shape: Rectangle | Circle
}
export type ColorName = "red" | "green" | "blue"
export interface Rectangle {
  kind: "rectangle",
  x: number
  y: number
  width: number
  height: number
}
export interface Circle {
  kind: "circle",
  x: number
  y: number
  radius: number
}

Then, create a JSON file “drawing.json” :

{
  "color": "green",
  "shape": {
    "kind": "circle",
    "x": 100,
    "y": 100,
    "radius": "wrong value"
  }
}

You are ready to check the JSON file:

$ npx @typeonly/checker-cli -s drawing.d.ts -t "Drawing" drawing.json
In property 'radius', value "wrong value" is not conform to number.

A mistake is detected in the JSON file. Fix it by replacing the value of the property "radius" with a valid number. For example: "radius": 50 . And run the command again:

$ npx @typeonly/checker-cli -s drawing.d.ts -t "Drawing" drawing.json
The JSON file is conform.

Good. The checker no longer complain.

The TypeOnly tool suite

In the previous section, the file containing types was parsed on the fly. It is a useful feature for a command line checker, but if you need to check several JSONs from your Node.js program, you’ll want to parse the typing once, then reuse the parsed stuff several time. Or, even better: you’ll want to parse the typing definition at compile time, save the parsed result, and then you no longer need the parser at runtime. This is the default way to work with TypeOnly. As a result, using typing metadata is a fast and lightweight process.

TypeOnly currently comes with 4 npm packages:

  • typeonly : The parser, implemented using ANTLR , is quite performant and small. It takes  .d.ts files and generates RTO files (RTO stands for Raw TypeOnly, the file extension is  .rto.json ).
  • @typeonly/reader : A lightweight API that helps to read RTO files.
  • @typeonly/checker : A lightweight API that checks JSON or JavaScript data.
  • @typeonly/checker-cli : A CLI that uses the parser and the checker on the fly.

Tutorial, part I: Parse TypeOnly code

In this tutorial we’ll see how to use TypeOnly in a JavaScript or a TypeScript project. In a new directory, install typeonly as a depency:

npm init
npm install typeonly --save-dev

Create a subdirectory src/ and copy into it our drawing.d.ts file given in the example above. Then, edit the file package.json and add an entry in the section "scripts" :

"scripts": {
  "typeonly": "typeonly -o dist-rto/ -s src/"
},

Now we can execute the TypeOnly parser via our script:

npm run typeonly

This command creates a file drawing.rto.json in a new directory dist-rto/ . A RTO ( .rto.json ) file contains all metadata extracted from a  .d.ts typing file. In the next sections we'll see two ways to use this generated RTO file.

Tutorial, part II: Use typing metadata at runtime

In order to navigate through RTO ( .rto.json ) files at runtime, we need the @typeonly/reader package:

npm install @typeonly/reader

Create a file src/main.js with the following content:

// src/main.js
const { readModules, literals } = require("@typeonly/reader")
async function main() {
  const modules = await readModules({
    modulePaths: ["./drawing"],
    baseDir: `${__dirname}/../dist-rto`
  })
const { ColorName } = modules["./drawing"].namedTypes
  console.log("Color names:", literals(ColorName, "string"))
}
main().catch(console.error)

If you write this code in a TypeScript source file, simply replace the require syntax with a standard import .

We can execute our program:

$ node src/main.js
Color names: [ 'red', 'green', 'blue' ]

Yes, it’s as easy as it seems: the list of color names is now available at runtime.

Notice that at runtime, our code doesn’t depend on the TypeOnly parser. We use @typeonly/reader which is a lightweight wrapper for  .rto.json files.

Tutorial, part III: How to check JSON data with TypeOnly (API version)

The package @typeonly/checker is built using @typeonly/reader . We need it:

npm install @typeonly/checker

Create a file src/check-main.js with the following content:

// src/check-main.js
const { createChecker } = require("@typeonly/checker")
const data = {
  "color": "green",
  "shape": {
    "kind": "circle",
    "x": 100,
    "y": 100,
    "radius": 50
  }
}
async function main() {
  const checker = await createChecker({
    readModules: {
      modulePaths: ["./drawing"],
      baseDir: `${__dirname}/../dist-rto`
    }
  })
  const result = checker.check("./drawing", "Drawing", data)
  console.log(result)
}
main().catch(console.error)

Execute this new program:

$ node src/check-main.js
{ valid: true }

As in the previous section, our code doesn’t depend on the TypeOnly parser at runtime. The checker uses the reader which just loads .rto.json file(s).

Conclusion

What is not covered by this article?

The TypeOnly language is described here . In particular, it allows imports and exports, which means that you can split your typing in several source files.

The APIs of the packages typeonly , @typeonly/reader and @typeonly/checker are not yet well documented. You will need the help of a TypeScript IDE to see all the options and provided data structures.

What’s next?

The help of contributors will be greatly appreciated. For us, TypeOnly is a side project and we don’t have a lot of time for it. However, we plan to work on several subjects in a near future:

/** doc comments */

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK