Skip to main content

Guide

Best TypeScript Config for SaaS Boilerplates 2026

Best TypeScript config for Next.js and Vite SaaS boilerplates in 2026: strict mode, bundler resolution, path aliases, project references, and typed env.

StarterPick Team
Hero image for Best TypeScript Config for SaaS Boilerplates 2026

TL;DR

The best TypeScript config for a 2026 SaaS boilerplate is strict by default, aligned with the runtime that actually builds the app, and boring enough that every template user can keep it enabled after cloning.

Use this baseline for a single-app Next.js boilerplate:

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": false,
    "skipLibCheck": true,
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [{ "name": "next" }],
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

Use the same philosophy for Vite, but remember Vite transpiles TypeScript and does not replace a real type-check step. Keep tsc --noEmit in CI.

What This 2026 Update Covers

TypeScript, Next.js, and Vite defaults keep moving, but the buyer question stays the same: does this boilerplate make production-safe TypeScript the normal path or does it leave every team to harden config after cloning?

This update is explicit about four decisions boilerplate buyers actually care about:

  1. the baseline tsconfig.json they should expect in a serious starter;
  2. when moduleResolution: "bundler" is right versus when node16 or nodenext is right;
  3. how Next.js, Vite, and monorepos differ;
  4. how typed environment variables fit into a production SaaS starter.

Quick Decision Table

Project shapeRecommended config postureWhy it fits
Single Next.js SaaS appstrict, moduleResolution: "bundler", isolatedModules, Next plugin, one @/* aliasMatches the official Next.js TypeScript integration and keeps imports stable without monorepo overhead.
Vite app or dashboardstrict, moduleResolution: "bundler", isolatedModules, tsc --noEmit in CIVite transpiles quickly; type checking should be a separate CI gate.
Turborepo / multi-package SaaSshared base config plus package-level extends, composite, and project references where packages depend on each otherTypeScript can understand package dependency order and incrementally rebuild dependent packages.
Node-only package or CLIconsider moduleResolution: "node16" or "nodenext"Node ESM rules differ from bundler rules; extension and package export behavior should match runtime Node.
Legacy starter with loose typesenable strict first, then add stricter flags in slicesstrict is the biggest quality jump; follow-on flags may need code cleanup.

Why TypeScript Config Is a Boilerplate Quality Signal

A SaaS boilerplate is not just a folder structure. It is a set of defaults that every future project inherits. The tsconfig.json is one of the clearest signals of whether that starter is built for production use or demo speed.

A good starter makes type safety the default path:

  • strict: true is enabled from the first commit.
  • env access is validated before runtime code depends on it.
  • imports use stable aliases instead of fragile relative paths.
  • the app has an explicit type-check command in CI.
  • monorepo examples show how package-level TypeScript configs fit together.

A weak starter often looks fine in the first hour but leaks risk later. Watch for strict: false, wide any casts, unchecked process.env.* reads, missing CI type checks, or path aliases that only work in one tool but not in tests, Storybook, scripts, or package builds.

If you are comparing paid and open-source starters, use TypeScript config as part of the buyer checklist alongside auth, billing, tests, migrations, and deploy scripts. For a broader buyer checklist, see the SaaS boilerplate buyers checklist and the Next.js starter kit guide.

The Baseline for Next.js Boilerplates

Next.js has first-class TypeScript support, including a custom TypeScript plugin for editor feedback and framework-specific checks. The official Next.js TypeScript documentation should be the source of truth for the base app config, especially when Next releases change generated defaults.

A production starter can still improve the baseline in a few ways:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitReturns": true,
    "moduleResolution": "bundler",
    "isolatedModules": true,
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

The important part is not that every starter uses the exact same JSON. The important part is that the config is deliberate and explained. A beginner-friendly starter can document why it uses one root alias. A team-oriented starter can include separate aliases for @/components/*, @/server/*, @/lib/*, and @/types/* if those names represent real architecture.

Do not add aliases as decoration. Add them when they make call sites clearer or prevent fragile import paths. A shallow alias map with ten names that all point into the same folder is noise. One or two meaningful aliases that separate UI, server, and shared code can improve review quality.

Strict Mode: Non-Negotiable for New Starters

strict: true enables TypeScript's strict family of checks. For a new SaaS boilerplate, leave it on.

The practical reason is simple: starter code becomes copied code. If the starter begins loose, every generated project starts with less protection and every downstream team has to decide whether to pay the migration cost later. If the starter begins strict, consumers learn the intended safety model while the codebase is still small.

Avoid these anti-patterns:

{
  "compilerOptions": {
    "strict": false
  }
}
// Bad: the starter teaches callers to bypass the type system.
const user = session.user as any;
const email = process.env.ADMIN_EMAIL!;

Prefer narrow types and explicit validation:

type SignedInUser = {
  id: string;
  email: string;
  role: "owner" | "admin" | "member";
};

function assertSignedInUser(value: unknown): SignedInUser {
  // In production code, pair this with a schema validator.
  if (!value || typeof value !== "object") {
    throw new Error("Expected signed-in user");
  }

  return value as SignedInUser;
}

Stricter Flags Worth Enabling

strict is the foundation, but it is not the end of the conversation.

noUncheckedIndexedAccess

This flag adds undefined to indexed access results. It is especially useful in SaaS code because app logic often indexes arrays of plans, roles, billing items, onboarding steps, or feature flags.

const plan = plans[0];

if (!plan) {
  throw new Error("No billing plan configured");
}

return plan.priceId;

Without the flag, it is easy to treat plans[0] as definitely present. With the flag, the config forces a guard before the value is used.

exactOptionalPropertyTypes

This flag distinguishes a missing property from a property explicitly set to undefined.

That matters for settings objects, patch payloads, feature flags, and integration configs:

type CheckoutOptions = {
  couponCode?: string;
};

const options: CheckoutOptions = {};

With exactOptionalPropertyTypes, couponCode?: string means the property can be omitted. It does not mean callers should pass { couponCode: undefined } and hope the downstream API treats it the same way.

noImplicitReturns and noFallthroughCasesInSwitch

These flags catch boring control-flow mistakes that become production bugs in billing, auth, onboarding, and provisioning code.

function planLabel(plan: "free" | "pro" | "enterprise") {
  switch (plan) {
    case "free":
      return "Free";
    case "pro":
      return "Pro";
    case "enterprise":
      return "Enterprise";
  }
}

For a starter, these flags are a way to teach safe defaults without adding a custom lint rule for every situation.

Module Resolution: Use Bundler Mode for Next.js and Vite

For modern bundled applications, moduleResolution: "bundler" is usually the correct setting. TypeScript added bundler module resolution to model the way bundlers handle package exports, conditions, and extensionless imports. This fits Next.js, Vite, and other bundled frontend applications better than older Node-only resolution assumptions.

Use:

{
  "compilerOptions": {
    "moduleResolution": "bundler"
  }
}

Do not use node16 or nodenext just because they sound newer. Those modes are for code that needs to match Node.js ESM resolution rules directly. If the code is bundled by Next.js, Vite, or another app bundler, bundler mode usually avoids unnecessary friction around explicit file extensions while still respecting modern package export maps.

A good starter should document the distinction:

  • app packages: moduleResolution: "bundler";
  • Node-only packages and CLIs: evaluate node16 or nodenext;
  • shared packages: pick based on how the package is built and consumed, not by copying the app config blindly.

Vite Starters Need a Separate Type-Check Gate

Vite has excellent TypeScript ergonomics, but its dev and build pipeline is designed for fast transpilation. Do not assume Vite's transform step replaces tsc type checking for a production starter.

A Vite SaaS starter should include scripts like this:

{
  "scripts": {
    "type-check": "tsc --noEmit",
    "build": "tsc --noEmit && vite build"
  }
}

For React app starters, this is one of the easiest ways to distinguish a demo template from a production-ready starter. If the template only proves that files transpile, it may still ship type errors.

For broader test expectations, pair this with testing in SaaS boilerplates and CI/CD pipeline setup for SaaS boilerplates.

Type-Safe Environment Variables

Environment variables are where many TypeScript starters quietly become unsafe. process.env.DATABASE_URL has a type that permits undefined, but application code often casts it to string and moves on.

A production starter should validate env vars once and export a typed object for the rest of the app.

For Next.js starters, the @t3-oss/env-nextjs pattern is widely used because it separates server and client variables and validates the runtime environment:

// src/env.ts
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";

export const env = createEnv({
  server: {
    DATABASE_URL: z.string().url(),
    STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
    STRIPE_WEBHOOK_SECRET: z.string().startsWith("whsec_"),
  },
  client: {
    NEXT_PUBLIC_APP_URL: z.string().url(),
  },
  runtimeEnv: {
    DATABASE_URL: process.env.DATABASE_URL,
    STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
    STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
    NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
  },
});

This is better than scattering non-null assertions throughout the app:

// Avoid this in starter code.
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

A starter that validates env vars also helps deployment. Missing DATABASE_URL, webhook secrets, auth keys, or site URLs fail early and loudly instead of becoming half-working production pages.

For auth-specific context, see authentication setup in Next.js boilerplates. For full stack decisions around database, auth, email, and payments, see the Next.js SaaS tech stack guide.

Path Aliases That Help Instead of Hide

The common Next.js alias is enough for many starters:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

This removes deep relative imports:

import { formatCurrency } from "@/lib/format";
import { Button } from "@/components/ui/button";

For larger starters, more explicit aliases can make the architecture clearer:

{
  "paths": {
    "@/components/*": ["./src/components/*"],
    "@/lib/*": ["./src/lib/*"],
    "@/server/*": ["./src/server/*"],
    "@/types/*": ["./src/types/*"]
  }
}

Only use the granular version if the folders actually represent meaningful seams. If @/server/* exists, client components should not import it. If @/types/* exists, the folder should not become a dumping ground for everything the codebase was too lazy to model near its owner.

Monorepo TypeScript Config

Monorepo starters need one more layer: a shared base config and package-level configs that extend it.

A common shape:

// packages/config/tsconfig/base.json
{
  "compilerOptions": {
    "target": "ES2022",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "moduleResolution": "bundler",
    "isolatedModules": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  }
}
// apps/web/tsconfig.json
{
  "extends": "@repo/config/tsconfig/nextjs.json",
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}
// packages/ui/tsconfig.json
{
  "extends": "@repo/config/tsconfig/react-library.json",
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src/**/*.ts", "src/**/*.tsx"]
}

Project references are useful when packages depend on each other and TypeScript needs to understand build order and declaration outputs. They are not mandatory for a single app, and they can make a simple starter harder to understand if there are no real package dependencies.

For most buyers, the question is not "does the starter use project references?" The better question is "does the starter's TypeScript setup match the architecture it is selling?" A monorepo starter should demonstrate package configs. A single-app starter should stay simple.

Type-Check Scripts Every Starter Should Ship

Add a dedicated type-check script even if the framework build also type-checks:

{
  "scripts": {
    "type-check": "tsc --noEmit",
    "type-check:watch": "tsc --noEmit --watch"
  }
}

Then use it in CI before build-heavy work:

- run: bun run lint
- run: bun run type-check
- run: bun run test:run
- run: bun run build

This sequence fails fast. A starter should make quality gates easy to keep, not something every buyer has to invent after the first production incident.

Boilerplate Red Flags

Be cautious when a TypeScript starter has these patterns:

Red flagWhy it matters
strict: falseThe starter is optimizing for demo smoothness over production safety.
No explicit type-check scriptCI may only prove that code transpiles or builds under one path.
Many as any casts in starter codeThe type system is being bypassed where it should teach safe patterns.
Unvalidated process.env.* readsMissing secrets or URLs can fail at runtime.
Aliases work in the app but not tests/scriptsThe config is tool-specific instead of repo-wide.
One monorepo config copied everywhereApp, package, CLI, and shared library packages may need different resolution and emit settings.
skipLibCheck: false used as a quality flexIt can waste CI time chasing dependency declaration issues; keep focus on your code unless you have a reason.

Before you pick or publish a SaaS boilerplate, check the TypeScript setup:

  • strict: true is enabled.
  • noUncheckedIndexedAccess is enabled or the starter explains why it is not.
  • exactOptionalPropertyTypes is enabled for new code or documented as a migration step.
  • moduleResolution matches the actual runtime/build tool.
  • path aliases work in app code, tests, and scripts.
  • environment variables are validated and exposed through a typed module.
  • CI runs lint, type-check, tests, and build separately.
  • monorepo packages use shared configs intentionally instead of copy-pasted app configs.
  • starter code avoids as any, blanket @ts-ignore, and secret-leaking env patterns.

Example: Production-Ready Next.js SaaS tsconfig.json

Here is a fuller app-level example that is a reasonable starting point for a Next.js SaaS starter in 2026:

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": false,
    "skipLibCheck": true,
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [{ "name": "next" }],
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

Treat this as a starting point, not a rule that overrides framework docs. When Next.js, Vite, TypeScript, or your runtime changes generated defaults, compare those docs first and adapt deliberately.

Bottom Line

The best TypeScript config for a SaaS boilerplate is the one that keeps production mistakes visible while the project is still cheap to fix. For Next.js and Vite starters in 2026, that means strict mode, bundler resolution, checked indexed access, typed env variables, a real CI type-check step, and monorepo references only where the architecture actually needs them.

TypeScript configuration will not make a bad starter good by itself. But a starter with weak TypeScript defaults is rarely production-ready in the places that matter: auth, billing, migrations, background jobs, and deployment gates.

Source Notes

Methodology

This refresh compares the existing canonical StarterPick guide against current TypeScript, Next.js, Vite, and typed-env documentation. The goal is not to invent a new URL; it is to make the existing guide answer TypeScript config and boilerplate-selection intent more clearly while keeping the advice conservative and source-backed.

The SaaS Boilerplate Matrix (Free PDF)

20+ SaaS starters compared: pricing, tech stack, auth, payments, and what you actually ship with. Updated monthly. Used by 150+ founders.

Join 150+ SaaS founders. Unsubscribe in one click.