7 minutes
Interfaces and Type-Only Imports
Last lesson, Generics, made one definition work for many types. This lesson covers the other half: one shape that many objects can promise to match. That shape is an interfaceA named list of the fields and methods an object must have, with no code of its own.. Along the way you hit the import type rule that create-vite's tsconfig.json switched on.
Writing an interface
An interface names the fields an object must have. Paste this into src/main.ts:
The coin and the gem match Pickup. respawns? has a ?, so it's optional, and the coin can leave it out. The key has no points, and the error says exactly which field is missing. In the browser the third line logs key is worth undefined.
An interface has no code. It's only a description, and like every type it's erased before the browser runs anything.
A class can promise to match an interface with the keyword implements. Break the promise and the compiler names the missing piece:
npx tsc reports error TS2420: Class 'Coin' incorrectly implements interface 'Pickup'. and then Property 'points' is missing in type 'Coin' but required in type 'Pickup'. Add points = 10; to the class and it passes.
The framework's query interface
Every system in the framework declares a query: which entities it wants. The shape of that query is an interface:
Five optional fields, each a list. ComponentType[] is a list of component classes, like the [Position] you wrote in the classes lesson. Now get it wrong twice:
The typo gets a "did you mean". The second error is noisier, but it says the same thing in its own way: Position isn't a list, because a list would have push and pop. Put it in brackets and it passes. Without the interface, requird would be a harmless extra field, and the system would quietly match every entity.
Branded handles can't be mixed up
In the types lesson, EntityId accepted any string. The framework's renderer, the part that draws, needs something stricter. When it makes a mesh (a 3D shape) or a light, it hands back a handleAn opaque token that stands for something another piece of code owns, like a mesh inside a 3D engine., a token that stands for the real object inside Babylon.js or Three.js, the two 3D engines it can draw with. Passing a mesh handle where a light handle belongs has to fail:
Each handle type has one field that exists only for the compiler. unique symbol is a type that matches nothing but itself, so a MeshHandle can never pass for a LightHandle. This trick is called a branded typeA type made unique by a marker field that exists only for the compiler, so two lookalike types can't be mixed up.. Get a real handle and try it:
MockRendererAdapter is a stand-in renderer that draws nothing, used for tests. The browser console shows the handle is really {__mockHandle: 'mesh:box'}. The __mesh field was never there. It was a type note, and the compiler used it to tell two shapes apart.
One interface, four renderers
The renderer itself is an interface. Here's its first line:
'babylon' | 'three' | 'babylon-lite' is a union: kind must be one of those three strings. Below it sit well over a hundred methods, like createMesh. Four classes implement it: BabylonAdapter, BabylonLiteAdapter, ThreeAdapter and the mock, which starts export class MockRendererAdapter implements RendererAdapter. The compiler checks every one of those methods against each class. Code that only asks for a RendererAdapter works with any of the four. The real ones come from their own entry points, separate import paths inside the package: @babylonjsmarket/ecs/babylon, @babylonjsmarket/ecs/babylon-lite and @babylonjsmarket/ecs/three, because each needs its 3D engine installed. The mock needs nothing, so it's the one you can run now.
import type and verbatimModuleSyntax
Type the renderer as the interface and import both names the obvious way:
The browser stops before running a line:
RendererAdapter is a type, so the package's JavaScript has no export by that name. The browser only asks for it because verbatimModuleSyntax is on in your tsconfig.json. With it on, Vite keeps every import exactly as written and erases only what you marked as a type. Delete that line from tsconfig.json and this same file runs, because Vite then drops names it sees used only as types. create-vite turns the setting on so what gets erased is written in the source instead of guessed, and TS1484 is tsc telling you before the browser does. Put the setting back and split the import in two:
import type is erased completely. npx tsc passes, and the console logs renderer kind: babylon, since the mock reports itself as Babylon.
In Sum
Your own Pickup interface caught a missing field with TS2345. ISystemQuery caught requird with TS2561 and a missing list with TS2740. Branded handles turned a mesh passed as a light into a compile error, and a plain import of RendererAdapter gave TS1484 in tsc and a SyntaxError in the browser until it became import type.
Next: What a Computer Can Do in 16 Milliseconds leaves the compiler behind and measures how much work fits in one frame.