Field guide / Practical workflows

Type-driven development with Dotdog

Start with the states and operations your domain permits. Use the compiler to reject invalid combinations, then keep the resulting contracts connected to spec and test evidence.

What type-driven development means here

Specs describe the intended rule. Types express the parts your language can enforce. Graphs connect those artifacts for navigation. Test-driven development is a separate practice; spelling out “type-driven” avoids confusing the two uses of TDD.

This walkthrough uses a small order workflow. A shipped order needs tracking data, and only a paid order can be passed to the shipping operation. It does not arrange delivery or prove a payment happened.

1. Use a compatible project

Dotdog's optional adapter requires Node.js 22.12+ and a trusted project-installed TypeScript 7.0.2. Core Dotdog does not require a compiler. If your application uses another version, use a separate scratch project rather than changing its compiler for this guide.

mkdir type-guide
cd type-guide
npm init -y
npm install --save-dev typescript@7.0.2
mkdir src

Create tsconfig.json with:

{
  "compilerOptions": {
    "strict": true,
    "noEmit": true,
    "target": "ES2022",
    "module": "NodeNext"
  },
  "include": ["src/**/*.ts"]
}

2. Model valid states before implementation

Create src/order.ts:

export type Draft = { state: "draft"; id: string };
export type Paid = { state: "paid"; id: string; receipt: string };
export type Shipped = {
  state: "shipped";
  id: string;
  receipt: string;
  tracking: string;
};
export type Order = Draft | Paid | Shipped;

export function ship(order: Paid, tracking: string): Shipped {
  return { ...order, state: "shipped", tracking };
}

The discriminant keeps receipt and tracking fields with the states that require them. A collection of unrelated booleans and optional fields would permit more invalid combinations. See TypeScript's discriminated-union documentation.

3. Inspect, save, and query a selected contract

npx dotdog@1.0.0 types inspect Order --file src/order.ts --project tsconfig.json --json > order-contract.json &&
npx dotdog@1.0.0 graph import order-contract.json --project types
npx dotdog@1.0.0 context 'src/order.ts#Paid' --dag .doghouse/imported/types.dag --json
npx dotdog@1.0.0 impact 'src/order.ts#Paid' --dag .doghouse/imported/types.dag --json
npx dotdog@1.0.0 graph audit --dag .doghouse/imported/types.dag --profile evidence --json

Only import a successful inspection: the shell's && prevents importing failed compiler output. Expect an ok: true inspection, an imported graph, and explicit partial coverage. Impact describes references in this selected graph, not every call site in your application.

Saved node evidence retains compiler/configuration/input identities. After changing source, configuration, compiler or dependencies, inspect again and replace the saved graph. A freshness audit checks recorded inputs; it cannot discover every new or unloaded file.

4. Try a call the compiler must reject

Create src/invalid.ts:

import { ship, type Draft } from "./order.js";

const draft: Draft = { state: "draft", id: "order-1" };
ship(draft, "TRACK-1");
npx dotdog@1.0.0 types inspect Order --file src/order.ts --project tsconfig.json --json

The configured project now fails checking and Dotdog exits nonzero. Repair the example by passing a Paid value with a receipt, then repeat inspection. Keep negative compile checks alongside runtime and integration tests in real applications.

5. Keep the boundaries honest

For supported aliases, processing bounds and evidence limitations, read the full type-contract reference. For the design rationale and newer compiler research, read the research notes.