DEV Community

Cover image for Three.js + TypeScript: Keeping It Functional (No Classes Needed)
Peter Riding
Peter Riding

Posted on

Three.js + TypeScript: Keeping It Functional (No Classes Needed)

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

2. Simple objects (like sizes)

Use let because we mutate it on resize:

let sizes = { width: window.innerWidth, height: window.innerHeight };
Enter fullscreen mode Exit fullscreen mode

3. Your functions

function animate(): void { ... }
Enter fullscreen mode Exit fullscreen mode

4. Event listeners

window.addEventListener('mousemove', (event: MouseEvent): void => { ... });
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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)