Three.js From Zero · Article s14-01

Vite + TypeScript from Zero

Vite + TypeScript from Zero is Article s14-01 of Three.js From Zero, a MasterAllArts free interactive lesson for artists learning creative 3D on the web.

← Three.js From ZeroS14-01 · Setup & Tooling

Season 14 · Article 01 · Setup & Tooling

A clean Three.js + TS project in one minute. No webpack, no rollup configs to learn. The setup every modern Three.js project starts with — strict types, HMR, path aliases, source maps.

The one-minute setup

npm create vite@latest my-three -- --template vanilla-ts
cd my-three
npm install three @types/three
npm run dev

Open http://localhost:5173. You have a Vite dev server, TypeScript checking, hot module replacement, and Three.js types — all under 20MB of node_modules.

The minimum tsconfig

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "skipLibCheck": true,
    "paths": { "@/*": ["./src/*"] }
  },
  "include": ["src"]
}

Three settings make all the difference:

  • strict: true — turn on all strict checks. Catches bugs Three.js code is famous for (forgetting type guards on raycast hits).
  • noUncheckedIndexedAccess: true — array access returns T | undefined. Forces you to guard the (rare) cases where it matters.
  • paths: { "@/*": ... } — clean imports: import { scene } from '@/lib/scene' beats ../../lib/scene.

HMR for Three.js

Vite's HMR works out of the box for code changes. But your scene state lives in objects that get garbage-collected on hot replace. The pattern:

// hmr-friendly setup
if (import.meta.hot) {
  import.meta.hot.dispose(() => {
    renderer.dispose();
    scene.traverse((obj) => {
      if (obj.geometry) obj.geometry.dispose();
      if (obj.material) obj.material.dispose();
    });
  });
}

Without this, every save leaks the previous renderer + scene + GPU resources. Five edits and your tab is at 1GB RAM.

Source maps for shader debugging

By default Vite produces sourcemaps in dev but not in production. For Three.js with inline GLSL, source maps make stack traces actually useful:

// vite.config.ts
export default {
  build: { sourcemap: true },
  // for inline GLSL strings, no extra plugin needed
};

vite.config additions for Three.js

import { defineConfig } from 'vite';
import glsl from 'vite-plugin-glsl';   // load .glsl files as strings

export default defineConfig({
  plugins: [glsl()],
  resolve: { alias: { '@': '/src' } },
  server: { open: true },              // auto-open browser
});

vite-plugin-glsl lets you write shaders in real .glsl files and import shader from './ripple.glsl'. Massively better than backtick strings — your editor highlights the GLSL.

Path aliases in action

// Before
import { setupRenderer } from '../../../lib/renderer';
import type { SceneConfig } from '../../../types';

// After
import { setupRenderer } from '@/lib/renderer';
import type { SceneConfig } from '@/types';

Common first-time pitfalls

"@types/three is missing types for things in /examples." @types/three covers the core. For loaders/controls/postprocessing: import from three/examples/jsm/... — they have types now (Three.js r150+).
"strict mode flags every Object3D check." Many Three.js APIs return Object3D | null (e.g., scene.getObjectByName). Use a guard helper: function assertMesh(o: Object3D | undefined): asserts o is Mesh { ... }.
"HMR breaks my scene state." See dispose pattern above. Or use Vite's import.meta.hot.accept() with a state-restore callback.

Exercises

  1. Set up a fresh project. Follow this article exactly. Time yourself. Goal: under 5 minutes from zero to spinning cube.
  2. Add path aliases. Refactor existing imports in a tutorial project to use @/*. Less import line noise.
  3. Add a custom Vite plugin. Auto-import all .glb files in /assets as a typed module. import duck from '@/assets/duck.glb'.