<!-- PkgPulse AI-readable guide source -->
<!-- Canonical: https://www.pkgpulse.com/guides/how-to-set-up-typescript-with-every-framework -->
<!-- Raw Markdown: https://www.pkgpulse.com/guides/how-to-set-up-typescript-with-every-framework/raw.md -->
<!-- Source path: content/guides/how-to-set-up-typescript-with-every-framework.mdx -->

---
og_image: "/images/guides/how-to-set-up-typescript-with-every-framework.webp"
title: "How to Set Up TypeScript with Every Major 2026"
description: "TypeScript configuration for Next.js, Remix, SvelteKit, Astro, Hono, Fastify, and plain Node.js. tsconfig settings, common gotchas, and the 2026 recommended."
date: "2026-03-08"
author: "PkgPulse Team"
tags: ["typescript", "nextjs", "remix", "sveltekit", "hono", "fastify", "2026"]
featured_comparison: "nextjs-vs-remix"
tier: 1
---

## TL;DR

**Every major framework ships TypeScript support out of the box in 2026.** Running `npm create vite@latest` or `npx create-next-app` generates a working `tsconfig.json`. The important customization is in `strict` settings, `moduleResolution`, and framework-specific type declarations. This guide covers the right tsconfig for each major framework and the gotchas that routinely cost hours.

## The Universal Base: tsconfig Settings That Apply Everywhere

Before framework-specific details, there's a set of tsconfig options that should be enabled in every project in 2026. These aren't optional — they catch real bugs that cost real time.

```json
{
  "compilerOptions": {
    "strict": true,                    // Enables all strict checks — do this first
    "noUncheckedIndexedAccess": true,  // arr[0] can be undefined — TypeScript will tell you
    "skipLibCheck": true,              // Skip type-checking node_modules — fast, safe
    "resolveJsonModule": true,         // import data from './data.json'
    "isolatedModules": true,           // Each file must be independently compilable
    "verbatimModuleSyntax": true       // TypeScript 5.0+ — correct ESM/CJS import types
  }
}
```

The two most commonly missed options are `noUncheckedIndexedAccess` and `verbatimModuleSyntax`. The first ensures `arr[0]` is typed as `T | undefined`, not just `T` — this catches index-out-of-bounds bugs at the type level. The second ensures you use `import type` for type-only imports, which prevents subtle bundling issues when types are erased.

The community `@tsconfig/bases` packages provide maintained starting points for different runtimes:

```bash
npm install -D @tsconfig/strictest    # Most strict — good baseline for any project
npm install -D @tsconfig/node20       # Node.js 20-specific optimizations
```

---

## Next.js

Next.js has the most polished TypeScript integration of any framework. Running `create-next-app --typescript` generates a complete `tsconfig.json` and a `next-env.d.ts` file that injects Next.js-specific types. You should not delete or edit `next-env.d.ts` — it's regenerated on every build.

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

The `"plugins": [{ "name": "next" }]` line adds the Next.js TypeScript language server plugin. This plugin provides editor completions for App Router conventions — it tells you when `generateStaticParams` has the wrong return type, when `metadata` exports don't match the `Metadata` interface, and when Server/Client Component boundaries are violated.

The App Router introduces type patterns worth knowing:

```typescript
// Page component props — params and searchParams
interface PageProps {
  params: { slug: string };
  searchParams: { [key: string]: string | string[] | undefined };
}

export default function Page({ params, searchParams }: PageProps) {
  // TypeScript knows params.slug is a string
}

// Route Handler (app/api/route.ts)
import type { NextRequest } from 'next/server';

export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  return Response.json({ id: params.id });
}

// Metadata export
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'My Page',
  description: 'Page description',
  openGraph: {
    images: [{ url: '/og.png', width: 1200, height: 630 }],
  },
};
```

**Key gotcha:** `noEmit: true` means TypeScript only type-checks, never compiles — Next.js's webpack/Turbopack pipeline handles actual transpilation. Don't change this. Running `tsc` directly will produce no output files, which surprises developers expecting a `dist` folder.

---

## Remix

Remix uses Vite as its build tool since v2, which changes the tsconfig requirements. Vite uses its own module resolution that differs from Node.js's, and the tsconfig must reflect this.

```json
{
  "include": ["**/*.ts", "**/*.tsx", "**/.server/**/*.ts", "**/.server/**/*.tsx"],
  "compilerOptions": {
    "lib": ["DOM", "DOM.Iterable", "ES2022"],
    "types": ["@remix-run/node", "vite/client"],
    "isolatedModules": true,
    "esModuleInterop": true,
    "jsx": "react-jsx",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "resolveJsonModule": true,
    "target": "ES2022",
    "strict": true,
    "allowJs": true,
    "skipLibCheck": true,
    "noEmit": true,
    "paths": {}
  }
}
```

Note `"moduleResolution": "Bundler"` (capital B). Remix's Vite setup uses bundler resolution, meaning TypeScript won't enforce Node.js's `.js` extension requirements on imports. This is correct for Remix — don't change it to `"node16"` or `"nodenext"`, which will break imports.

```typescript
// Remix type patterns — loader and action typing
import type { LoaderFunctionArgs, ActionFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';

type LoaderData = { user: { id: string; name: string } };

export async function loader({ params }: LoaderFunctionArgs) {
  const user = await getUser(params.id!);
  return json<LoaderData>({ user });
}

export default function UserPage() {
  const { user } = useLoaderData<typeof loader>();
  // user is correctly typed as { id: string; name: string }
  return <div>{user.name}</div>;
}
```

**Key gotcha:** Remix's `useLoaderData<typeof loader>()` pattern infers types from the loader return value. This only works correctly when the loader uses `json()` from `@remix-run/node` and you pass the loader function as the type parameter — not a manually typed interface.

---

## SvelteKit

SvelteKit's TypeScript setup is the most "managed" of any framework. SvelteKit generates a `.svelte-kit/tsconfig.json` automatically, and your project's `tsconfig.json` extends it. The generated file is maintained by SvelteKit and updated when you run `vite dev` or `vite build`.

```json
// tsconfig.json — your file (minimal, extends the generated one)
{
  "extends": "./.svelte-kit/tsconfig.json",
  "compilerOptions": {
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "verbatimModuleSyntax": true
  }
}

// .svelte-kit/tsconfig.json — generated, do not edit
// Contains paths, includes, and SvelteKit-specific compiler options
```

SvelteKit's most important TypeScript feature is the `app.d.ts` ambient declarations file:

```typescript
// src/app.d.ts — ambient type declarations for SvelteKit
declare global {
  namespace App {
    interface Locals {
      user: { id: string; email: string } | null;  // Available in hooks and load functions
    }

    interface PageData {
      // Common data available on all pages
    }

    interface Error {
      message: string;
      code?: string;  // Custom error shape
    }
  }
}

export {};
```

After defining these, SvelteKit's load functions and hooks are fully typed:

```typescript
// +page.server.ts — typed via app.d.ts
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ locals, params }) => {
  // locals.user is typed as { id: string; email: string } | null
  if (!locals.user) throw redirect(302, '/login');

  return { item: await getItem(params.id) };
};
```

Run `npx svelte-check --tsconfig ./tsconfig.json` in CI to catch type errors in `.svelte` files — the standard `tsc` command doesn't check `.svelte` file types.

---

## Astro

Astro's TypeScript setup is similar to SvelteKit — the framework generates type declarations, and you extend a managed config. The key difference is Astro's `src/env.d.ts` file, which imports the framework's type definitions.

```typescript
// src/env.d.ts — required for Astro types
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />
```

```json
// tsconfig.json
{
  "extends": "astro/tsconfigs/strict",
  "compilerOptions": {
    "strictNullChecks": true,
    "noUnusedLocals": true,
    "paths": {
      "@/*": ["./src/*"],
      "@components/*": ["./src/components/*"]
    }
  }
}
```

Astro ships three preset configs: `astro/tsconfigs/base`, `astro/tsconfigs/strict`, and `astro/tsconfigs/strictest`. Start with `strict` — it's the recommended default.

```typescript
// Astro component typing
---
// The frontmatter script is TypeScript
interface Props {
  title: string;
  description?: string;
  tags: string[];
}

const { title, description = '', tags } = Astro.props;
// TypeScript infers: title is string, description is string, tags is string[]
---

<article>
  <h1>{title}</h1>
  {description && <p>{description}</p>}
</article>
```

Run `npx astro check` in CI for `.astro` file type checking. Like SvelteKit, standard `tsc` doesn't check the framework-specific file extensions.

---

## Hono (Edge and Node.js)

Hono is a TypeScript-first framework designed to run on Cloudflare Workers, Deno, Bun, and Node.js. Its tsconfig differs by target runtime.

```json
// tsconfig.json — Hono on Cloudflare Workers
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ES2020",
    "moduleResolution": "bundler",
    "strict": true,
    "lib": ["ES2020"],
    "types": ["@cloudflare/workers-types"],
    "isolatedModules": true,
    "resolveJsonModule": true,
    "noEmit": true
  },
  "include": ["src", "worker-configuration.d.ts"]
}
```

```json
// tsconfig.json — Hono on Node.js
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "lib": ["ES2022"],
    "types": ["node"],
    "outDir": "dist",
    "rootDir": "src",
    "declaration": true,
    "sourceMap": true
  },
  "include": ["src"]
}
```

Hono's generic request context typing is one of its best features — you can type route handlers end-to-end:

```typescript
import { Hono } from 'hono';

type Variables = {
  userId: string;
  user: { id: string; email: string };
};

const app = new Hono<{ Variables: Variables }>();

app.use('/protected/*', async (c, next) => {
  const token = c.req.header('Authorization');
  const user = await verifyToken(token);
  c.set('user', user);      // TypeScript knows the shape
  await next();
});

app.get('/protected/profile', (c) => {
  const user = c.get('user');  // Typed as { id: string; email: string }
  return c.json({ user });
});
```

---

## Fastify

Fastify is TypeScript-first and its type system is particularly rich — request body, params, query string, response, and headers can all be typed per route.

```json
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "dist",
    "rootDir": "src",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
```

```typescript
// Typed route with Fastify's generic system
import Fastify from 'fastify';

const fastify = Fastify({ logger: true });

interface CreateUserBody {
  name: string;
  email: string;
}

interface UserParams {
  id: string;
}

fastify.post<{ Body: CreateUserBody }>('/users', {
  schema: {
    body: {
      type: 'object',
      required: ['name', 'email'],
      properties: {
        name: { type: 'string' },
        email: { type: 'string', format: 'email' },
      },
    },
  },
}, async (request, reply) => {
  const { name, email } = request.body; // Typed as CreateUserBody
  return { user: await createUser({ name, email }) };
});
```

```json
// package.json scripts for Fastify TypeScript
{
  "scripts": {
    "dev": "tsx watch src/server.ts",
    "build": "tsc",
    "start": "node dist/server.js",
    "typecheck": "tsc --noEmit"
  }
}
```

---

## Common TypeScript Configuration Issues and How to Fix Them

These are the five configuration problems that cause the most wasted hours in real TypeScript projects. Each one has a specific fix, and understanding why the fix works prevents you from hitting it again.

**1. moduleResolution conflicts — Bundler vs NodeNext vs Node10**

The most common tsconfig error in 2026 is using the wrong `moduleResolution` for your runtime environment. TypeScript 5.0 introduced `"Bundler"` mode alongside the existing `"NodeNext"` and legacy `"Node10"` (previously called `"Node"`). These are not interchangeable.

`"NodeNext"` enforces Node.js ESM rules: relative imports must include the `.js` extension, even when writing `.ts` files. This is correct for Node.js servers that use native ESM (`"type": "module"` in `package.json`), but it breaks Vite, Next.js, and Remix projects because those bundlers resolve extensions themselves.

`"Bundler"` mode is designed for projects processed by a bundler: it allows bare imports without extensions, follows `exports` and `imports` fields in `package.json`, but does not enforce Node.js's specific module loading algorithm. Use `"Bundler"` for any project that goes through Vite, webpack, Turbopack, or esbuild. Use `"NodeNext"` for Node.js servers that emit and run JavaScript directly.

The symptom of using `"NodeNext"` in a Vite project: TypeScript errors like `"An import path cannot end with a '.ts' extension"` or `"Relative import paths need explicit file extensions"`.

**2. Path alias setup that breaks at runtime**

TypeScript's `paths` configuration maps import aliases for the TypeScript compiler but does not affect the runtime. If you write `import { db } from '@/db'` and configure `"paths": { "@/*": ["./src/*"] }` in tsconfig, TypeScript will be happy — but Node.js will throw `Cannot find module '@/db'` at runtime unless you also configure your bundler.

For Next.js, the `paths` in tsconfig are automatically picked up by webpack/Turbopack — no extra configuration needed. For Vite projects, you need to add the alias to `vite.config.ts` as well:

```typescript
// vite.config.ts — must mirror tsconfig paths
import { resolve } from 'path';

export default {
  resolve: {
    alias: {
      '@': resolve(__dirname, './src'),
    },
  },
};
```

For Node.js servers that compile with `tsc` and run the output directly, you need `tsconfig-paths` or similar at runtime, or use `tsc-alias` as a post-compilation step to rewrite import paths in the output.

**3. skipLibCheck tradeoffs**

`skipLibCheck: true` tells TypeScript to skip type-checking declaration files (`.d.ts` files) in `node_modules`. The tradeoff: you get faster type checking, but type errors in third-party packages are silently ignored. In practice, `skipLibCheck: true` is the right choice for almost all application code — third-party type errors are usually conflicts between two packages' type definitions, not bugs in your code, and fixing them requires upstream changes you cannot make.

The one case where you might want `skipLibCheck: false`: library authors who publish their own type declarations. If your package ships `.d.ts` files, you want to verify those are internally consistent.

**4. The strict: true vs individual flags debate**

`"strict": true` is a shorthand that enables a bundle of individual checks: `strictNullChecks`, `strictFunctionTypes`, `strictBindCallApply`, `strictPropertyInitialization`, `noImplicitAny`, `noImplicitThis`, and `alwaysStrict`. Enabling `strict: true` is almost always the right approach for new projects.

The individual flags debate arises when onboarding TypeScript into an existing JavaScript codebase. Enabling all of `strict: true` at once on a large existing codebase produces thousands of errors. The pragmatic approach: enable `noImplicitAny` and `strictNullChecks` first, fix those errors, then enable the remaining strict flags. These two flags catch the most serious bugs. The remaining flags (`strictFunctionTypes`, `strictBindCallApply`, etc.) are valuable but less likely to surface real runtime bugs in application code.

**5. Declaration file conflicts**

The symptom: TypeScript errors like `"Duplicate identifier 'X'"` or `"Cannot redeclare block-scoped variable"` in files you did not write. This usually means two packages are declaring the same global type, or a `types` field in tsconfig is including global type definitions that conflict.

The fix: check your `"types"` array in `compilerOptions`. If you have `"types": ["node", "@cloudflare/workers-types"]` simultaneously, you will get conflicts because both declare `fetch`, `Request`, `Response`, and other globals differently. Include only the types appropriate for your runtime. For projects that genuinely target multiple runtimes, split into multiple tsconfig files using project references.

---

## TypeScript Performance Optimization for Large Codebases

`tsc` slows down in large codebases, and the slowdown is not linear — a codebase with 500 files can easily take 30 seconds for a type check that took 2 seconds at 100 files. Here are the techniques that make a measurable difference.

`--incremental` mode is the first and easiest optimization. Adding `"incremental": true` to your `compilerOptions` causes TypeScript to write a `.tsbuildinfo` file that stores information about the compilation. On subsequent runs, TypeScript only re-checks files that changed and their dependents. The first run is the same speed, but subsequent runs can be 5-10x faster. This is already enabled by default in Next.js's generated tsconfig. For other projects, add it explicitly and ensure `.tsbuildinfo` is gitignored but preserved in CI caches between runs.

Project references are the industrial-strength solution for monorepos and large applications with distinct subsystems. Instead of one tsconfig that covers the entire codebase, you split into multiple tsconfig files, each covering a subsystem, with explicit dependency declarations between them:

```json
// tsconfig.json — root
{
  "references": [
    { "path": "./packages/shared" },
    { "path": "./packages/api" },
    { "path": "./packages/web" }
  ]
}

// packages/api/tsconfig.json
{
  "compilerOptions": {
    "composite": true,     // Required for project references
    "outDir": "dist",
    "rootDir": "src"
  },
  "references": [
    { "path": "../shared" }  // api depends on shared
  ]
}
```

With project references and `tsc --build` (or `tsc -b`), TypeScript only rebuilds packages whose source files or dependencies changed. In a monorepo where the web package depends on the api package, changing a web-only file does not trigger a recheck of the api package.

`include` and `exclude` tuning is frequently overlooked. The default `"include": ["**/*"]` includes everything in the project root, including test fixtures, generated files, and documentation that does not need type checking in every run. Be explicit:

```json
{
  "include": ["src/**/*.ts", "src/**/*.tsx"],
  "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
}
```

For CI, consider running type checking separately from tests and excluding test files from the main tsconfig. Create a separate `tsconfig.test.json` that extends the main config and adds test files back in — this way your production type check is fast, and your test type check runs independently.

`skipLibCheck: true` reduces check time by skipping type declaration files in `node_modules`. This is the easiest single-flag win and is safe for application code. Pairing it with the `--noEmit` flag in CI (type-check only, no output) keeps the check focused on finding type errors rather than producing artifacts.

For teams where `tsc` is the bottleneck in CI, the check time for TypeScript tooling options like `ts-node` vs `tsx` vs `tsc` directly is worth reviewing — see [tsx vs ts-node vs Bun in 2026](/guides/tsx-vs-ts-node-vs-bun-2026) for a direct comparison of TypeScript execution speed across tools.

---

## Type-Only Imports and the verbatimModuleSyntax Flag

The `import type` syntax and the `verbatimModuleSyntax` compiler flag are TypeScript features that are simple to understand but significantly impact bundle output and bundler performance. Many codebases are not using them correctly in 2026.

When you write `import { User } from './types'`, TypeScript knows `User` is a type and will erase it from the compiled output. However, the import statement itself may still appear in the output depending on your module system, causing bundlers to potentially include the module in the bundle even though nothing from it is used at runtime. When you write `import type { User } from './types'`, TypeScript and bundlers can guarantee at parse time that this import will produce no runtime code and can be safely tree-shaken.

`verbatimModuleSyntax`, introduced in TypeScript 5.0, enforces this discipline automatically. With the flag enabled, TypeScript requires that any import used only as a type must use `import type`. If you write `import { User } from './types'` and use `User` only in type positions, TypeScript will error and tell you to use `import type { User } from './types'` instead. This is the flag's main value: it makes the "erase at compile time" behavior explicit and verifiable rather than inferred.

Migrating an existing codebase to use `verbatimModuleSyntax` typically produces a wave of errors on first enabling it. The errors are all the same pattern: `import { SomeType }` where `SomeType` is only used in type positions. The fix is mechanical:

```typescript
// Before
import { User, createUser } from './users';  // User is type-only, createUser is runtime

// After
import type { User } from './users';
import { createUser } from './users';
```

Most editors with TypeScript language server support will automatically suggest or apply this fix. Running `tsc --noEmit` with `verbatimModuleSyntax: true` after enabling the flag will surface every import that needs to be updated. In codebases with good test coverage, enabling this flag, running the auto-fix, and re-running tests is usually a safe one-step migration.

The bundler performance impact is real but secondary to correctness. By using `import type` consistently, bundlers like Vite and webpack can skip processing modules that are only referenced as types during development server startup and HMR updates. In large TypeScript codebases, this translates to faster cold starts during development.

For the full picture on TypeScript tooling in 2026, including how `verbatimModuleSyntax` interacts with different build tools, see [the state of TypeScript tooling in 2026](/guides/state-of-typescript-tooling-2026).

---

## Common tsconfig Mistakes

These mistakes come up repeatedly in real codebases and each one creates real problems.

```json
// Mistake 1: Wrong moduleResolution for bundler-based projects
"moduleResolution": "node"
// Fix: use "bundler" for Vite, Next.js, Remix; use "NodeNext" for Node.js

// Mistake 2: Not enabling strict
"strict": false
// This allows implicit any, skips null checks — bugs guaranteed in production

// Mistake 3: Missing noUncheckedIndexedAccess
// Without it: users[0].name — crashes if users is empty, TypeScript won't warn you
"noUncheckedIndexedAccess": true

// Mistake 4: target too old for the runtime
"target": "ES5"
// Produces verbose polyfilled output for runtimes that support ES2020+
// Use ES2020+ for Node.js 16+, Cloudflare Workers, modern browsers

// Mistake 5: Not separating type checking from building in CI
// Wrong:
"build": "tsc && next build"
// Right:
"typecheck": "tsc --noEmit",
"build": "next build"
// Type checking and building are independent — run them in parallel

// Mistake 6: verbatimModuleSyntax missing
// Without it, TypeScript allows: import { User } from './types'
// When User is a type, this causes runtime errors in strict ESM
"verbatimModuleSyntax": true
// Enforces: import type { User } from './types'
```

---

## The 2026 Recommended Baseline

For any new TypeScript project in 2026, regardless of framework:

```json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "verbatimModuleSyntax": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "isolatedModules": true
  }
}
```

Add the framework-specific settings on top of this baseline. The `moduleResolution` depends on your runtime: `"bundler"` for Vite/Next.js/Remix, `"NodeNext"` for Node.js APIs. The `target` depends on your deployment environment: `ES2020` for broad compatibility, `ES2022` for modern Node.js.

Check full package health and download trends for [TypeScript and related packages on PkgPulse](/packages/typescript). For a framework comparison, see [Next.js vs Remix on PkgPulse](/compare/nextjs-vs-remix). See also the [new wave of TypeScript-first libraries in 2026](/guides/new-wave-typescript-first-libraries-2026) for how TypeScript-native design is reshaping the npm ecosystem.

*See also: [Fastify vs Hono](/compare/fastify-vs-hono) and [Next.js vs Remix](/compare/next-vs-remix)*

If type-check speed becomes the bottleneck after setup, compare [tsgo vs tsc](/guides/tsgo-vs-tsc-typescript-7-go-compiler-2026) before changing the compiler in CI.
