Skip to main content

TypeScript

Every package in Refract is TypeScript-first. Each one has its own tsconfig.json tuned to what it needs to produce — a CommonJS library, an ES module bundle, or a browser SPA. Here’s a tour of what’s set up and why.

Backend (apps/backend/tsconfig.json)

The backend compiles to CommonJS for Node.js. It’s intentionally minimal — the goal is to get a clean dist/ for production, and the test suite is excluded from compilation entirely:
{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "outDir": "dist",
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "moduleResolution": "node"
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts"],
  "exclude": ["node_modules", "dist", "**/*.spec.ts", "**/__tests__/**", "scripts/**"]
}
A few things worth noting:
  • esModuleInterop: true lets you write import Stripe from 'stripe' instead of import * as Stripe from 'stripe' for CommonJS modules that don’t have a default export.
  • resolveJsonModule: true allows importing .json files directly — handy for things like jest.config.json.
  • Test files are excluded because ts-jest handles their compilation separately at test time.

Frontend (apps/frontend/tsconfig.app.json)

The frontend uses a split config: tsconfig.app.json for the application source and tsconfig.node.json for Vite’s config file. The top-level tsconfig.json just references both. tsconfig.app.json is where the interesting settings live:
{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedSideEffectImports": true
  }
}
Key things here:
  • moduleResolution: "bundler" is the modern mode for Vite/esbuild — it understands bare specifiers and TypeScript extensions without the old Node resolution quirks.
  • noEmit: true means TypeScript is purely a type-checker here. Vite handles the actual compilation.
  • noUnusedLocals and noUnusedParameters are turned on — TypeScript will error if you declare a variable or parameter you never use. This catches a lot of accidental dead code.
  • noUncheckedSideEffectImports prevents imports that exist only for side effects without an explicit acknowledgement — helps keep the import graph intentional.

Shared (apps/shared/tsconfig.json)

The shared package outputs both declarations and source maps alongside CommonJS JS, because it’s consumed by both the backend and the tool packages:
{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "moduleResolution": "node",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  }
}
declaration: true and declarationMap: true produce the .d.ts files that let the backend and tool packages get full type information when importing from apps/shared. If you add a new type or schema to shared, run make build on the shared package before the backend will pick it up.

Tool packages (apps/tools/<tool>/<impl>/tsconfig.json)

All tool packages (BullMQ, SQS, Pino, Console, StatsD, etc.) share the same config shape as apps/shared — CommonJS output with declarations and source maps:
{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "moduleResolution": "node",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  }
}
This is intentional — all tool packages are plug-in libraries and need to ship types so the backend can use them with full type safety.

Building

# Build a single package
make build module=backend
make build module=frontend

# Build everything in the workspace
make build-all
The build-all command is what CI uses. Note that backend tests depend on the compiled dist/ of all tool packages — so if you’re running backend tests locally and something isn’t found, a make build-all will usually fix it.
💡 Tip: if you’re adding a new tool package, make sure to run make pnpm-install after adding it to pnpm-workspace.yaml. Otherwise the workspace won’t know about it and make build-all won’t include it.

What’s next?

  • Linter — ESLint and Prettier configuration.
  • Testing — how ts-jest handles TypeScript compilation in tests.
  • Monorepo — how packages reference each other across the workspace.