Skip to content

Type Safety

typesugar follows the same model as the TypeScript ecosystem: fast builds, background IDE checking, CI correctness gate. This guide explains how the three layers work together and how to configure each one.

The Three-Layer Model

┌──────────────────────────────────────────────────────────────┐
│                                                              │
│  Layer 1: BUILD          Fast, no typechecking               │
│  (Vite, esbuild, etc.)   Macros expand, types are stripped   │
│                                                              │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  Layer 2: IDE            Background typechecking             │
│  (VS Code, Cursor)       Red squiggles as you type           │
│                                                              │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  Layer 3: CI             Full correctness gate               │
│  (tsc --noEmit)          Broken types don't merge            │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Layer 1: Build (Fast, No Typechecking)

Modern bundlers (Vite, esbuild, Webpack, Rollup) strip types without checking them. They parse your TypeScript, remove type annotations, and emit JavaScript — no tsc involved. This is why dev servers start in milliseconds.

typesugar fits into this layer: the unplugin runs macro expansion during the build's transform hook. Macros expand, extension methods rewrite, specialization inlines — but no typechecking happens unless you opt in.

What you get: Fast builds with macro expansion.

What you don't get: Type errors. If you pass a string where a number is expected, the build succeeds silently.

Layer 2: IDE (Background Typechecking)

Your editor runs tsserver in the background. It performs incremental typechecking as you type, showing red squiggles for type errors.

typesugar's language service plugin (typesugar/language-service) hooks into this to transform your code before the IDE typechecks it. The IDE sees the macro-expanded version and reports errors against that.

What you get: Real-time type errors in your editor, including errors in macro-expanded code.

What you need: The language service plugin in tsconfig.json:

json
{
  "compilerOptions": {
    "plugins": [{ "name": "typesugar/language-service" }]
  }
}

Layer 3: CI (Correctness Gate)

tsc --noEmit runs the full TypeScript compiler without emitting output. This catches every type error, including ones the IDE might miss (e.g., cross-file issues in large projects). Run this in CI to prevent broken types from merging.

What you get: Complete type safety. Nothing passes that tsc wouldn't accept.

What you need:

bash
tsc --noEmit && npm run build

The Exception: tsc with ts-patch

When you build directly with tsc (via ts-patch), all three layers collapse into one. The TypeScript compiler typechecks AND transforms in the same pass. You get type errors during the build itself — no separate tsc --noEmit needed.

Why Pre-Macro Code Is Type-Incomplete

If you've used @derive(Eq) on a class, the .equals() method doesn't exist in your source code — it's generated by the macro. Before macros run, the code is intentionally type-incomplete:

typescript
@derive(Eq)
class Point {
  constructor(
    public x: number,
    public y: number
  ) {}
}

const p1 = new Point(1, 2);
const p2 = new Point(3, 4);

p1.equals(p2);
// ^ Before macro expansion: "Property 'equals' does not exist on type 'Point'"
// ^ After macro expansion: ✅ works — @derive(Eq) generated it

This is expected behavior. The language service plugin runs macros first so the IDE sees the expanded code and doesn't show false errors.

Without the language service plugin, your IDE would flag every macro-generated method as missing. This is the most common "something's broken" report from new users — and the fix is always to add the plugin to tsconfig.json.

How Macros Validate Types

During expansion, type-aware macros use the TypeScript TypeChecker to make decisions:

  • @derive(Eq) inspects field types to generate correct comparison logic
  • summon<Eq<Point>>() resolves to a concrete instance by searching the type graph
  • Extension methods check receiver types to find matching extension functions

When a macro detects a problem, it reports a diagnostic:

error[TS9001]: No instance found for `Eq<Color>`
  --> src/palette.ts:12:5
   |
10 | interface Palette { primary: Color; accent: Color }
11 |
12 | p1 === p2
   |     ^^^ Eq<Palette> requires Eq<Color>
   |
   = note: field `primary` has type `Color` which lacks Eq
   = help: Add @derive(Eq) to Color, or provide @instance Eq<Color>

These diagnostics use typesugar's error code range (TS9001–TS9999). They work like regular TypeScript errors in the IDE and CLI.

When to Use strict: true

The strict option runs tsc on the macro-expanded output at the end of the build. This catches type errors that macros might introduce — for example, a macro generating code with a wrong return type.

typescript
// vite.config.ts
typesugar({
  strict: true,
});
Scenariostrict: falsestrict: true
Dev server startupFastSlower (typecheck at build end)
Macro expansion errorsReported via ctx.reportError()Same + post-expansion tsc errors
Type errors in macro outputNot caught until tsc --noEmitCaught during build
Recommended forDevelopmentCI, production builds

Rule of thumb: Use strict: false for development (fast iteration), strict: true or tsc --noEmit for CI.

Minimal (works for any project)

yaml
# .github/workflows/ci.yml
steps:
  - run: npm ci
  - run: npx ts-patch install # if using tsc directly
  - run: tsc --noEmit # typecheck
  - run: npm run build # build (macros expand here)
  - run: npm test # tests

With strict mode (no separate tsc step)

yaml
steps:
  - run: npm ci
  - run: npm run build # typesugar({ strict: true }) handles typechecking
  - run: npm test

Monorepo with Turborepo

json
{
  "pipeline": {
    "typecheck": {
      "dependsOn": ["^build"],
      "command": "tsc --noEmit"
    },
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["build"]
    }
  }
}

Summary

WhatWhereTypechecks?Speed
Dev buildVite/esbuild/WebpackNo (unless strict: true)Fast
IDEVS Code/Cursor + language serviceYes (background)Incremental
CI gatetsc --noEmitYes (full)Depends on project size
tsc buildts-patchYes (built-in)Depends on project size
strict: trueAny build toolYes (at build end)Adds tsc pass

Released under the MIT License.