TypeScript with Vite: Setting Up Fast Build Pipelines
AI generated
<T>
type
TypeScript · Vite · Build Tools · Frontend Tooling
TypeScript with Vite: Setting Up Fast Build Pipelines
From esbuild to vite-plugin-checker: type safety without the drag

Vite transpiles TypeScript at blazing speed during development via esbuild, but deliberately skips type-checking so Hot Module Replacement and dev server startup stay fast. This article shows how to bring type errors back into view with vite-plugin-checker, configure vite.config.ts cleanly, and set up a minimal Vite plus TypeScript project for admin widgets and frontend tools.

13 min read esbuild · vite-plugin-checker · tsconfig Vite 5 · TypeScript 5 · Node 20

1. Why Vite is the right choice for TypeScript build pipelines

Vite has established itself as the standard tool for modern frontend build pipelines because it unites two previously conflicting goals: a dev server that starts in milliseconds, and a production build that ships highly optimized code via Rollup. For teams who work with PHP and Magento alongside TypeScript-based admin widgets, storefront extensions, or standalone build scripts, Vite significantly reduces the friction between development and deployment, since the same configuration file handles both modes.

The decisive difference from older bundlers like Webpack lies in native ESM usage during development: the browser loads modules directly, Vite transforms only on demand, and Hot Module Replacement stays consistently fast even as projects grow. For TypeScript projects, that concretely means type-checking and module transformation are treated as separate concerns, which feels unfamiliar at first but turns out to be a deliberate architectural decision once it is clear which tool handles which task.

2. How Vite handles TypeScript: esbuild instead of tsc

For transforming TypeScript files during development, Vite does not use the official TypeScript compiler tsc, but esbuild, a transpilation tool written in Go that runs ten to a hundred times faster than tsc. esbuild simply strips the type annotations from the source code and translates modern syntax into a format the browser or Node.js understands, without performing any type-checking at all. This separation is not a compromise but a deliberate design decision: type-checking is inherently a process that must analyze the entire program structure, whereas pure transpilation can happen in isolation per file and can therefore be parallelized.

For production builds, Vite delegates to Rollup internally, which likewise does not check types and instead focuses on bundling, tree-shaking, and code-splitting. In practice this means a vite build can complete successfully even if the code contains type errors, as long as the syntax is valid. Anyone relying solely on vite build as a quality gate is therefore potentially shipping broken bundles that only surface at runtime.

3. The catch: Vite skips type-checking during the dev server and HMR

The speed of esbuild has a direct side effect: in the default setup, the Vite dev server reports no TypeScript errors in the browser or terminal whatsoever. A mistyped function call, a missing property on an interface, or a wrong return type annotation goes unnoticed both on save and on hot reload, as long as the resulting JavaScript stays syntactically valid. For developers coming from Webpack with ts-loader or from older Create React App setups, this is a noticeable break from familiar behavior, since type errors there traditionally interrupted the build process visibly.

In practice, this means type errors slip unnoticed into the git history and only surface at the next tsc invocation, an IDE restart, or, in the worst case, in the CI pipeline. Especially with smaller admin dashboard widgets that rarely get fully type-checked through the IDE because developers work primarily in the browser with the dev server running, this creates a deceptive sense of safety: the dev server is running, so everything seems fine, even though the code already contains inconsistent types in several places.

4. Configuring vite.config.ts with proper types: defineConfig and UserConfig

The Vite configuration file itself should be created as vite.config.ts and written using the helper function defineConfig from the vite package, rather than exporting a raw object. At runtime, defineConfig is nearly an identity function; its real value lies in the TypeScript signature: it gives the IDE full autocompletion and type-checking for every configuration option, from server.proxy through build.rollupOptions to plugin-specific extensions.

For more complex projects where the configuration differs by mode (development, production, test), defineConfig alternatively accepts a function that receives a ConfigEnv object with mode and command and returns a UserConfig object. This variant is the cleanest way to define, for example, different base paths for development and deployment into a Magento static assets directory, without manually reading process-wide environment variables and losing type safety along the way.


// vite.config.ts: typed configuration with mode-dependent options
import { defineConfig, type ConfigEnv, type UserConfig } from 'vite';
import path from 'node:path';

export default defineConfig(({ mode, command }: ConfigEnv): UserConfig => {
  const isProd = mode === 'production';

  return {
    // Different base path for the built widget in a Magento static folder
    base: isProd ? '/static/frontend/Mironsoft/default/en_US/widgets/' : '/',
    resolve: {
      alias: {
        '@': path.resolve(__dirname, 'src'),
      },
    },
    build: {
      target: 'es2022',
      sourcemap: !isProd,
      rollupOptions: {
        output: {
          entryFileNames: isProd ? '[name].[hash].js' : '[name].js',
        },
      },
    },
    server: {
      port: 5173,
      // Proxy admin API calls to a local Magento instance during development
      proxy: {
        '/rest': { target: 'https://mironsoft.test', secure: false, changeOrigin: true },
      },
    },
  };
});

5. vite-plugin-checker: surfacing type errors in the dev overlay and terminal

vite-plugin-checker closes exactly the gap described in section 3: the plugin starts the TypeScript compiler in the background in a separate worker thread, parallel to the actual Vite dev server, and reports any type errors found both as an overlay directly in the browser and as formatted output in the terminal. Because the check runs asynchronously in its own process, it blocks neither the dev server startup nor the responsiveness of Hot Module Replacement, which means the central speed advantage of esbuild is fully preserved.

Configuration happens through a single plugin instance in vite.config.ts, typically enabled with { typescript: true }, optionally extended with ESLint or Vue template checking in the same overlay. In larger monorepos, it is also worth setting the option typescript: { tsconfigPath: './tsconfig.json' } to explicitly control which configuration file gets checked, since Vite does not always automatically find the right tsconfig.json in nested workspace structures.


// vite.config.ts: surface type errors without blocking the dev server
import { defineConfig } from 'vite';
import checker from 'vite-plugin-checker';

export default defineConfig({
  plugins: [
    checker({
      // Runs tsc in a separate worker thread, reports via overlay and terminal
      typescript: {
        tsconfigPath: './tsconfig.json',
      },
      // Optional: lint alongside type-checking in the same overlay
      eslint: {
        lintCommand: 'eslint "src/**/*.{ts,tsx}"',
        useFlatConfig: true,
      },
      overlay: {
        initialIsOpen: false,
      },
    }),
  ],
});

6. tsconfig.json for Vite: isolatedModules and other pitfalls

Vite processes every file individually and in isolation, without knowledge of the rest of the type system, which is why tsconfig.json must set "isolatedModules": true. This setting forbids TypeScript constructs that can only be transpiled correctly with full program analysis, most notably const enum and certain forms of re-exporting pure types without the export type keyword. Ignoring this rule risks silently wrong JavaScript output that produces unexpected values at runtime without esbuild ever reporting an error.

In addition, "moduleResolution": "bundler" should be set so TypeScript simulates the same module resolution as Vite itself, including support for package exports fields without file extensions. "skipLibCheck": true further speeds up type-checking noticeably by skipping re-checking of declaration files from node_modules, and "noEmit": true ensures tsc is used exclusively for checking, while Vite and esbuild remain solely responsible for the actual code output.


{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "skipLibCheck": true,
    "noEmit": true,
    "resolveJsonModule": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src", "vite.config.ts"]
}

7. Practical setup: a minimal Vite plus TypeScript project for an admin dashboard widget

For a standalone admin dashboard widget that later gets embedded as a static bundle into a Magento backend page or a Hyva storefront page, a lean project structure is enough: a src directory with a typed entry file, a vite.config.ts with library mode configuration, and a tsconfig.json following the rules from section 6. Vite's library mode, enabled via build.lib, produces a single importable bundle instead of a full HTML application, one that can be versioned and embedded independently of the rest of the frontend.

The entry file should be consistently typed, including explicit return types for exported functions, since exactly these signatures later get checked by vite-plugin-checker and in the CI pipeline. For DOM access, the non-null assertion operator is only advisable where the element is guaranteed to exist; otherwise explicit null checks are preferable to avoid runtime errors in production admin widgets, which frequently get embedded in different Magento backend contexts.


// src/main.ts: typed entry module for an admin dashboard widget
interface OrderStat {
  label: string;
  value: number;
}

async function fetchOrderStats(): Promise<OrderStat[]> {
  const response = await fetch('/rest/V1/mironsoft/dashboard/order-stats');
  if (!response.ok) {
    throw new Error(`Failed to load order stats: ${response.status}`);
  }
  return response.json() as Promise<OrderStat[]>;
}

function renderStats(container: HTMLElement, stats: OrderStat[]): void {
  container.innerHTML = stats
    .map((stat) => `<div class="stat"><span>${stat.label}</span><strong>${stat.value}</strong></div>`)
    .join('');
}

function mountWidget(selector: string): void {
  const container = document.querySelector<HTMLElement>(selector);
  if (!container) {
    // Explicit null check instead of a non-null assertion
    console.warn(`Widget container "${selector}" not found`);
    return;
  }

  fetchOrderStats()
    .then((stats) => renderStats(container, stats))
    .catch((error: unknown) => {
      console.error('Order stats widget failed to load', error);
    });
}

mountWidget('#order-stats-widget');

{
  "name": "@mironsoft/order-stats-widget",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "typecheck": "tsc --noEmit",
    "ci": "npm run typecheck && npm run build"
  },
  "devDependencies": {
    "typescript": "^5.5.0",
    "vite": "^5.4.0",
    "vite-plugin-checker": "^0.7.0"
  }
}

8. Production builds and CI/CD: securing type-checking reliably

Because vite build does not check types and vite-plugin-checker is primarily meant for the local development experience, an explicit tsc --noEmit step belongs in every CI pipeline, regardless of whether GitHub Actions, GitLab CI, or another system is in use. This step runs synchronously, fails with a clear exit code on type errors, and reliably prevents broken code from reaching the main branch or, worse, a production deployment.

A sensible package.json script explicitly separates typecheck from build, so both steps can run independently in pipeline stages and be parallelized when needed, reducing overall pipeline runtime. In projects with several admin widgets or micro-frontends, it is also worth adding an --incremental flag for tsc, which caches a .tsbuildinfo file and noticeably speeds up repeated CI runs without compromising the reliability of the check.

9. Vite workflows compared: dev server, checker plugin, and CI

The three approaches to type-checking presented in this article are not mutually exclusive, but complement each other at different stages of development. The table below summarizes when each approach makes sense and what to watch out for when combining them.

Approach Speed When errors surface Suitability for CI
Vite dev server without type-checking Very fast (esbuild) Never automatically, only on manual checks Unsuitable
Vite + vite-plugin-checker Fast, parallel in a worker Immediately in the browser overlay and terminal Not sufficient on its own
Separate tsc --noEmit in CI Slower, full program analysis Only at commit/push in the pipeline Recommended as a gate

In practice, combining vite-plugin-checker for fast feedback during development with a separate tsc --noEmit step as a CI gate has proven the most robust approach. The dev server stays consistently fast, type errors still surface nearly in real time, and the CI pipeline additionally guarantees that no one accidentally merges a locally ignored overlay warning into the main branch.

Mironsoft

TypeScript tooling, build optimization, and frontend infrastructure

Ready to set up Vite and TypeScript properly?

We set up your Vite pipeline with proper types, integrate vite-plugin-checker for fast feedback in the dev overlay, and secure your production build with a reliable tsc check in the CI pipeline.

Vite setup audit

Analysis of vite.config.ts, tsconfig.json, and build times

Type-safe configuration

Integrating defineConfig, UserConfig, and vite-plugin-checker cleanly

CI/CD hardening

Setting up tsc --noEmit as a reliable type-checking gate

10. Summary

The core question with TypeScript and Vite is not whether type-checking happens, but where. Vite itself transpiles TypeScript via esbuild and deliberately skips type-checking, both in the dev server and during the production build via Rollup. vite-plugin-checker closes this gap for local development by running TypeScript in a separate worker thread in parallel and reporting errors non-blockingly in the overlay and terminal. For tsconfig.json, isolatedModules, moduleResolution: "bundler", and noEmit are the three central settings that make Vite's file-isolated processing reliably possible in the first place.

A minimal setup for an admin dashboard widget consists of a typed vite.config.ts with defineConfig and UserConfig, a clearly typed entry file, and separate package.json scripts for dev, build, and typecheck. The decisive safety net remains a separate tsc --noEmit call in the CI pipeline, because neither the dev server nor the production build alone guarantees that the shipped code is type-correct.

TypeScript with Vite: Setting Up Fast Build Pipelines - The Essentials at a Glance

esbuild instead of tsc

Vite transpiles via esbuild during both the dev server and the build, without ever checking types.

vite-plugin-checker

Parallel type-checking in a worker thread, errors in the browser overlay and terminal, without slowing down the dev server.

Type-safe configuration

defineConfig and UserConfig for vite.config.ts, isolatedModules and noEmit in tsconfig.json.

CI as a gate

A separate tsc --noEmit step in the pipeline, independent of vite build.

11. FAQ: TypeScript with Vite

1Why does Vite not check TypeScript types during development?
esbuild only strips type annotations instead of checking them. Type-checking needs project-wide analysis and would negate esbuild's speed, so it stays a separate step.
2What exactly does vite-plugin-checker do?
Starts tsc in a separate worker thread parallel to the dev server and reports type errors as a browser overlay and terminal output, without blocking the dev server.
3Does vite-plugin-checker slow down the dev server?
No, the check runs asynchronously in its own process. Dev server and HMR stay equally fast, errors appear with a small delay in the overlay.
4How does defineConfig differ from a plain object export configuration?
defineConfig provides full type inference and autocompletion. A raw object without defineConfig allows silent typos in option names without reliable checking.
5What is UserConfig and what is it used for?
The return type defineConfig expects, describing every valid Vite configuration field. Should be explicitly annotated when writing a function-based configuration.
6Why does Vite require isolatedModules in tsconfig.json?
Vite processes each file in isolation without project-wide type information. isolatedModules ensures every file stays individually compilable.
7Can I use const enum in a Vite plus TypeScript project?
No, isolatedModules forbids const enum. A regular enum or a union type of string literals is the common alternative.
8Is vite-plugin-checker enough for the CI pipeline, or do I also need tsc --noEmit?
A separate tsc --noEmit step in CI remains necessary, since vite-plugin-checker is primarily meant for local development.
9How do I build a minimal Vite plus TypeScript setup for an admin dashboard widget?
A src directory with a typed entry file, vite.config.ts in library mode via build.lib, and a tsconfig.json with isolatedModules, moduleResolution bundler, and noEmit.
10What esbuild limitations should I know about for TypeScript in Vite?
No type-checking, no const enum without violating isolatedModules, partially different handling of experimental decorator metadata than tsc. A separate tsc step or vite-plugin-checker makes up for this.