Introduction — tcm-ts

1. Introduction

tcm-ts keeps your TypeScript in a codebase instead of a folder of files. You write code in ordinary .ts files, as always; when it typechecks, you add it, and each function, type and constant is stored by a hash of its code. From then on, names are just labels on those hashes.

That one change has a lot of consequences, and they're the reason to use it:

  • Renaming is free. tcm mv lib.parse lib.parseConfig changes a label. No code is edited, nothing breaks, every use shows the new name.
  • Changes carry through, or tell you. tcm update re-checks everything that uses what you changed. Whatever still typechecks follows along automatically; whatever doesn't is put on a todo list, with the reason. Nothing is ever half-updated.
  • Nothing to build. Node runs the stored code directly (JSX is compiled as it loads), and a production build is named by the app's hash and reused. Tests and lint findings are cached by hash: a test only runs again if its code, or something it calls, changed, and results travel with tcm push and pull, so another machine doesn't run them again.
  • Less to manage. Apps bundle only what they reach, so there's no dead code to eliminate; definitions that refer to each other are stored together, so there are no circular imports; and one codebase holds any number of apps and packages, with no package.json to maintain and no monorepo tooling.
  • History by meaning. tcm diff shows what changed in your code, not in your files: reformatting and renaming locals aren't changes, and renames are recognised as renames. Every change can be undone.
  • Docs that can't go stale. Links and examples in your doc comments are code too: they follow renames and updates, and examples are run, so a broken one is reported.
  • Web apps without an API layer. Write a function that reads a database and call it from browser code. tcm works out what runs on the server and what runs in the browser, and connects them, with no routes, fetch calls or "use server". Because calls go by hash, a page can never call the wrong version of the server, and deploying and rolling back are moving a name.

Each of these, where it stops, and the test that proves it: what tcm does away with.

It's Unison's codebase model, brought to plain TypeScript: no new language, no new syntax, and the code runs on Node as it is.

1.1 How it's different

One small shop app, both ways. As a JavaScript project, its code is in files, in folders, in packages, and each tool reads the files to make what it makes:

The shop app as a JavaScript project: definitions in files, folders and packages, six tools, and four outputs, with five problems marked[close]

As a codebase, each value, type and function is a definition of its own, and an output is what it starts from reaches:

The same app as a codebase: names pointing at hashes, the definitions and their references, and four outputs, each starting from one definition, with what became of the five problems[close]

When one value changes, a project checks, tests, bundles and ships again every file that imports its file. A codebase redoes only what reaches it:

The project as TAX_RATE changes: all of @shop/web, five tools and both bundles are redone[close]

The codebase as TAX_RATE changes: five definitions get new hashes, one test runs, and the server and the npm package are unchanged[close]

Press a diagram to see it full size.

1.2 What it looks like

// scratch.ts
/** Square a number. */
export const square = (x: number): number => x * x;

square(12);
$ tcm check
scratch.ts
  + new       square : (x: number) => number

    7 │ square(12)
      ⧩ 144

$ tcm add
  + new       square : (x: number) => number

square(12) on its own line is a watch expression: tcm runs it and shows its value, like a REPL inside your file. square itself is now in the codebase, and any other file can use it with import { square } from "#codebase".

1.3 Where next

built from c482a14e