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:
- the baseline
tsconfig.jsonthey should expect in a serious starter; - when
moduleResolution: "bundler"is right versus whennode16ornodenextis right; - how Next.js, Vite, and monorepos differ;
- how typed environment variables fit into a production SaaS starter.
Quick Decision Table
| Project shape | Recommended config posture | Why it fits |
|---|---|---|
| Single Next.js SaaS app | strict, moduleResolution: "bundler", isolatedModules, Next plugin, one @/* alias | Matches the official Next.js TypeScript integration and keeps imports stable without monorepo overhead. |
| Vite app or dashboard | strict, moduleResolution: "bundler", isolatedModules, tsc --noEmit in CI | Vite transpiles quickly; type checking should be a separate CI gate. |
| Turborepo / multi-package SaaS | shared base config plus package-level extends, composite, and project references where packages depend on each other | TypeScript can understand package dependency order and incrementally rebuild dependent packages. |
| Node-only package or CLI | consider moduleResolution: "node16" or "nodenext" | Node ESM rules differ from bundler rules; extension and package export behavior should match runtime Node. |
| Legacy starter with loose types | enable strict first, then add stricter flags in slices | strict 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: trueis 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
node16ornodenext; - 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 flag | Why it matters |
|---|---|
strict: false | The starter is optimizing for demo smoothness over production safety. |
No explicit type-check script | CI may only prove that code transpiles or builds under one path. |
Many as any casts in starter code | The type system is being bypassed where it should teach safe patterns. |
Unvalidated process.env.* reads | Missing secrets or URLs can fail at runtime. |
| Aliases work in the app but not tests/scripts | The config is tool-specific instead of repo-wide. |
| One monorepo config copied everywhere | App, package, CLI, and shared library packages may need different resolution and emit settings. |
skipLibCheck: false used as a quality flex | It can waste CI time chasing dependency declaration issues; keep focus on your code unless you have a reason. |
Recommended Starter Checklist
Before you pick or publish a SaaS boilerplate, check the TypeScript setup:
-
strict: trueis enabled. -
noUncheckedIndexedAccessis enabled or the starter explains why it is not. -
exactOptionalPropertyTypesis enabled for new code or documented as a migration step. -
moduleResolutionmatches 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
- TypeScript TSConfig Reference, including
strict,moduleResolution,noUncheckedIndexedAccess, and related compiler options. Accessed May 16, 2026: typescriptlang.org/tsconfig - TypeScript Project References documentation. Accessed May 16, 2026: typescriptlang.org/docs/handbook/project-references.html
- TypeScript 5.0 release notes for bundler module resolution context. Accessed May 16, 2026: typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html
- Next.js TypeScript configuration documentation. Accessed May 16, 2026: nextjs.org/docs/app/api-reference/config/typescript
- Vite TypeScript feature documentation. Accessed May 16, 2026: vite.dev/guide/features.html#typescript
t3-envrepository andcreateEnvusage. Accessed May 16, 2026: github.com/t3-oss/t3-env
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.
