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
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;
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.
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.target is not a Three.js object
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.
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:
-
strictNullChecks— catches all the| null | undefinedissues above -
noImplicitAny— forces type annotations on function parameters, catching theObject3DvsMeshissue at the call site