II Business Logic

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.tsGenerated from the manifest. Rewritten on every run; do not edit.
types/index.d.tsWritten 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
EntityThe stored object: the declared properties and the system ones.
StateWhat a transition receives: Entity plus DISCARD and TRAILERS.
<Operation>Input, <Operation>OutputFrom each operation’s input and output: CreateInput, CreateOutput. An operation that declares no input has no such type and takes null, as approve does.
ContextThe 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:

--pathThe Context root; . by default.
--environmentThe environment the Context is read for.
--quietPrint 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.

Search
Search the documentation