TypeScript + Three.js Without a Build Step — Import Maps, @types/three & Common Pitfalls

Build tools are useful but not required. With modern import maps and ES modules, you can get full Three.js type safety — autocomplete, type errors, go-to-definition — directly in VS Code without running a single npm run build command.

1. Import Maps for Build-Free ES Modules

An import map is a <script type="importmap"> block in your HTML that maps bare module specifiers (like 'three') to real URLs. All modern browsers support this natively:

<script type="importmap">
{
  "imports": {
    "three":          "https://cdn.jsdelivr.net/npm/three@0.160/build/three.module.js",
    "three/addons/":  "https://cdn.jsdelivr.net/npm/three@0.160/examples/jsm/"
  }
}
</script>

<script type="module">
// ✅ Works directly in the browser — no bundler needed
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

const renderer = new THREE.WebGLRenderer({ antialias: true });
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 1000);
// ...
</script>

2. TypeScript Types Without tsc

The TypeScript language server in VS Code (Pylance / tsserver) reads type definitions even for plain .js files if you add a jsconfig.json or a // @ts-check comment. Install the types package locally (for the editor only — it's not shipped to users) and point your config to it:

# In your project directory:
npm install --save-dev three @types/three

# This installs only to node_modules — nothing is bundled or shipped
// jsconfig.json — enables types in VS Code for .js files
{
  "compilerOptions": {
    "checkJs": true,
    "strict": false,
    "moduleResolution": "bundler",
    "types": ["three"]
  },
  "include": ["**/*.js"],
  "exclude": ["node_modules"]
}

Now VS Code knows the full Three.js type tree. You get autocomplete, hover documentation, and type errors in your JavaScript files — without a tsconfig.json or a compilation step.

3. JSDoc as a Typed-JavaScript Alternative

If you want zero Node.js dependency (no package.json at all), you can add types via JSDoc annotations and reference the type definitions from a CDN:

// @ts-check
/// <reference types="https://cdn.jsdelivr.net/npm/@types/three/index.d.ts" />

/** @type {import('three').PerspectiveCamera} */
const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 1000);

/**
 * @param {import('three').Mesh} mesh
 * @param {import('three').Vector3} direction
 * @returns {void}
 */
function moveMesh(mesh, direction) {
  mesh.position.add(direction);
}

CDN type references work in VS Code 1.88+ with the "typescript.preferences.useAliasesForRenames" engine. VS Code downloads the .d.ts file once and caches it. This is the approach used in all the simulations on this site — no build tooling, full IntelliSense.

4. Common Type Pitfalls in Three.js Projects

Null canvas reference after DOMContentLoaded
document.getElementById('canvas') returns HTMLElement | null. Three.js's WebGLRenderer constructor expects HTMLCanvasElement, not HTMLElement | null. Cast explicitly: canvas as HTMLCanvasElement or use a null assertion inside a guard: if (!(el instanceof HTMLCanvasElement)) return;
Object3D.getObjectByName returns unknown type
scene.getObjectByName('mesh') returns Object3D | undefined. If you know it's a Mesh, use a type assertion: scene.getObjectByName('mesh') as THREE.Mesh | undefined and handle the undefined case before accessing geometry or material.
Material properties don't exist on the base type
mesh.material.color doesn't compile because the base Material type has no color property. You must narrow to the concrete type: (mesh.material as THREE.MeshStandardMaterial).color.set('#ff0000') or declare the mesh as THREE.Mesh<THREE.BufferGeometry, THREE.MeshStandardMaterial> in the first place.
Event handler event.target is not a Three.js object
When using Raycaster, the intersection object property is typed as Object3D, not the specific mesh subtype. Use intersection.object instanceof THREE.Mesh as a type guard before accessing mesh-specific properties.
Uniforms typed too loosely in ShaderMaterial
ShaderMaterial.uniforms is typed as { [uniform: string]: IUniform } — very broad. Define a typed const and spread it into the material to get autocomplete for your specific uniform names: const uniforms = { uTime: { value: 0 } } as const.

5. Strict Mode Recommendations

Even in a JSDoc-only setup, enabling "strict": true in jsconfig.json catches the null/undefined pitfalls above at editor-time. For Three.js projects specifically, the most useful strict sub-options are: