If you're following the Three.js Journey or learning Three.js with plain JavaScript, you might wonder how TypeScript fits in without rewriting everything.
You are already learning Three.js the fun way — just functions, variables at the top of the file, and zero classes. The good news? Adding TypeScript doesn’t force you to change any of that. It just quietly adds safety so you catch bugs before they ruin your day.
Why TypeScript actually helps with Three.js
Three.js is amazing, but 3D code is picky. Pass the wrong vector? Your mesh disappears. Mix up a camera and a scene? Nothing renders. TypeScript spots those mistakes the second you type them.
It’s especially handy for:
- All the vector and matrix math that has to be exactly right
- WebXR sessions and input sources (those APIs are a nightmare without types — just note that WebXR support and types evolve quickly in browsers)
- Your animation loops, event handlers, and object tracking
The best part? Your code stays exactly the same structure you’re used to.
From plain JavaScript to TypeScript (seriously, tiny changes)
Here’s what your setup probably looks like right now:
const canvas = document.querySelector('canvas.webgl');
const scene = new THREE.Scene();
const sizes = { width: window.innerWidth, height: window.innerHeight };
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100);
camera.position.z = 3;
const renderer = new THREE.WebGLRenderer({ canvas });
renderer.setSize(sizes.width, sizes.height);
const clock = new THREE.Clock();
const mesh = new THREE.Mesh(/* ... */);
function animate() {
const elapsedTime = clock.getElapsedTime();
mesh.rotation.y = elapsedTime * 0.5;
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
And here’s the exact same thing with TypeScript sprinkled in:
import * as THREE from 'three';
const canvas = document.querySelector<HTMLCanvasElement>('canvas.webgl');
if (!canvas) throw new Error('Canvas element not found');
let sizes = {
width: window.innerWidth,
height: window.innerHeight
};
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100);
camera.position.z = 3;
const renderer = new THREE.WebGLRenderer({ canvas });
renderer.setSize(sizes.width, sizes.height);
const clock = new THREE.Clock();
const mesh = new THREE.Mesh(/* ... */);
function animate(): void {
const elapsedTime = clock.getElapsedTime();
mesh.rotation.y = elapsedTime * 0.5;
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
That’s literally it.
The only four things you’ll actually type most of the time
1. The canvas
const canvas = document.querySelector<HTMLCanvasElement>('canvas.webgl');
if (!canvas) throw new Error('Canvas element not found');
2. Simple objects (like sizes)
Use let because we mutate it on resize:
let sizes = { width: window.innerWidth, height: window.innerHeight };
3. Your functions
function animate(): void { ... }
4. Event listeners
window.addEventListener('mousemove', (event: MouseEvent): void => { ... });
Full working example (copy-paste ready)
import * as THREE from 'three';
let sizes = {
width: window.innerWidth,
height: window.innerHeight
};
const canvas = document.querySelector<HTMLCanvasElement>('canvas.webgl');
if (!canvas) throw new Error('Canvas element not found');
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100);
camera.position.z = 3;
scene.add(camera);
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// geometry + material + mesh
const geometry = new THREE.BoxGeometry(1, 1, 1, 2, 2, 2);
const material = new THREE.MeshStandardMaterial({ color: 0xa778d8, roughness: 0.4 });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// lights
scene.add(new THREE.AmbientLight(0xffffff, 0.5));
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(5, 5, 5);
scene.add(dirLight);
const clock = new THREE.Clock();
function animate(): void {
const elapsedTime = clock.getElapsedTime();
mesh.rotation.x = elapsedTime * 0.1;
mesh.rotation.y = elapsedTime * 0.15;
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
// events
window.addEventListener('mousemove', (event: MouseEvent): void => {
// add cursor logic here if you need mouse interaction
});
window.addEventListener('resize', (): void => {
sizes.width = window.innerWidth;
sizes.height = window.innerHeight;
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});
animate();
Going further (still functional)
Loading textures (cleanest way):
const textureLoader = new THREE.TextureLoader();
const colorTex = await textureLoader.loadAsync('/textures/color.jpg');
colorTex.colorSpace = THREE.SRGBColorSpace;
material.map = colorTex;
Good practices for bigger projects
Call geometry.dispose(), material.dispose() and texture.dispose() when you’re done with them. Remove event listeners in a cleanup function. TypeScript makes these teardown functions much safer.
Quick setup (Vite + TypeScript)
package.json (latest as of March 2026):
"dependencies": { "three": "^r183" },
"devDependencies": {
"typescript": "^5.3",
"vite": "^5"
}
Important note on types: Three.js has shipped its own TypeScript definitions for years. Try without @types/three first — many people no longer need the extra package.
Run npm run type-check whenever you want to see what TypeScript thinks.
Wrapping up
Your code stays functional. No classes, no big refactor. Just a few types that make everything feel more solid (and safer at runtime).
Drop your current Three.js Journey chapter 1 code into TypeScript and see what happens — I bet you’ll fix three things in the first minute and you will feel like a wizard Harry.
Top comments (0)