Use TypeScript
The shop’s team wants the editor and CI to catch a mistyped property, or a call that no longer
fits another component’s contract, before anything runs. The component’s files are .ts; their
types are generated from the manifests, and checked with tsc.
§An operation in TypeScript
approve of store.orders, as Create a Component writes
it:
// components/store.orders/operations/approve.ts
import type { State } from '../types/index.d.ts'
export async function transition (input: null, entry: State) {
if (entry.status !== 'pending')
return new Error('NOT_PENDING')
entry.status = 'approved'
return entry
} An operation, an event, a receiver, a guard or a run command may be a .ts file. There is no
build step and no output directory: the file beside the manifest is the file that runs.
$ npm run restart
$ npx toa call store.orders.approve "{ query: { id: '5b1ad5d8a4c54e1dbb2a4a0b6e1f0c11' } }" The exported name says the type, and the second parameter’s name says the scope.
§Generate the types
$ npx toa types npm run env runs it, and npm run restart runs that first. It writes, for every component of the Context, a types directory beside its manifest:
| File | |
|---|---|
types/toa.d.ts | Generated from the manifest. Rewritten on every run; do not edit. |
types/index.d.ts | Written once and then left alone. Re-exports toa.d.ts; your own types go here. |
and the same pair in a types directory beside context.toa.yaml, for what the whole
application shares.
What a component’s types contain:
| Type | |
|---|---|
Entity | The stored object: the declared properties and the system ones. |
State | What a transition receives: Entity plus DISCARD and TRAILERS. |
<Operation>Input, <Operation>Output | From each operation’s input and output: CreateInput, CreateOutput. An operation that declares no input has no such type and takes null, as approve does. |
Context | The operation context of this component: local with its own operations, remote with every component of the application, and what its extensions add. |
Only what a manifest states is generated. An operation that declares no output returns unknown — one more reason to declare outputs. Calls through context.remote are typed from the
other component’s manifest, so a change to its contract shows up as a type error in its callers.
Run toa types again after changing a manifest; npm run restart does. Options:
--path | The Context root; . by default. |
--environment | The environment the Context is read for. |
--quiet | Print nothing. |
§Another component’s types
Each types directory is a package: a component’s is @components/<directory>, the
application’s is named after the Context. With the components listed as npm workspaces, as the package.json toa create wrote lists them, one component imports another’s types by name:
import type { Entity as Order } from '@components/store.orders' §Types in JavaScript
A JavaScript component gets the same types through JSDoc:
// components/store.orders/operations/approve.js
/** @import { State } from '../types/index.d.ts' */
/**
* @param {null} input
* @param {State} entry
*/
export async function transition (input, entry) { /* … */ } §Check the types
Nothing checks types when a component runs. Check them with tsc, in CI and in the editor:
$ npm run typecheck It runs tsc with the tsconfig.json toa create wrote beside the Context, which covers every
component and the steps of the tests:
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"target": "esnext",
"types": ["node"],
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"erasableSyntaxOnly": true
},
"include": ["components", "features"]
} The last three options enforce the three rules below, so that tsc refuses what would fail at
run time.
§Three rules
The types are removed when a file is loaded and nothing else is compiled. So:
A relative import names the file that exists, with its extension:
import { totals } from './lib/totals.ts' A type is imported with import type. import { type Context } leaves an import statement
behind, which fails at run time for a module that only has types.
import type { Context } from '../types/index.d.ts' No enum, no namespace, no constructor parameter property. They are code rather than
annotations, and the module is refused when it is loaded:
operations/create.ts: TypeScript enum is not supported in strip-only mode. §What you may run into
Two files that resolve to the same name are refused:
Component at '/app/components/store.orders' has more than one operations/create: create.js and create.ts A component that is published in an npm package must be transpiled to JavaScript before it
is published: types are not removed from files under node_modules. A component your
application contains may stay TypeScript.