How to Set Up TypeScript with Every Major Framework
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.
{
"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:
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.
{
"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:
// 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.
{
"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.
// 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.
// 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:
// 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:
// +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.
// src/env.d.ts — required for Astro types
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />
// 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.
// 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.
// 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"]
}
// 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:
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.
// 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"]
}
// 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 }) };
});
// package.json scripts for Fastify TypeScript
{
"scripts": {
"dev": "tsx watch src/server.ts",
"build": "tsc",
"start": "node dist/server.js",
"typecheck": "tsc --noEmit"
}
}
Common tsconfig Mistakes
These mistakes come up repeatedly in real codebases and each one creates real problems.
// 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:
{
"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. For a framework comparison, see Next.js vs Remix on PkgPulse.
See the live comparison
View nextjs vs. remix on PkgPulse →