Craters started as plain JavaScript. The rewrite to TypeScript wasn’t something I planned from the beginning — it happened gradually, then all at once. And it changed how I designed the framework, not just how I typed it.
Here’s what I actually learned.
When you write TypeScript for your own application, you own all the types. A framework is different — you’re writing types for code you haven’t seen yet. Users will pass in their own components, their own systems, their own world configurations.
Getting generics right so the framework is helpful without becoming a straitjacket took real iteration. The wrong approach:
// Too rigid — forces users into your specific class hierarchy
class System {
update(entities: MyEntity[]): void {}
}
The better approach:
// Flexible — users implement however they want, still get full type checking
interface System {
execute(entities: Entity[], delta: number): void;
}
The rule I settled on: prefer interface over class for a framework’s public surface. It gives users the freedom to implement things their way while still catching type errors.
Starting with strict: false and tightening later is genuinely painful. You end up fighting the compiler on code that’s already shipped.
I turned on strict: true in tsconfig.json from day one:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true
}
}
More verbose in places, but significantly more trustworthy. If you’re building something other people will depend on, strict mode isn’t a style choice — it’s the baseline.
TypeScript compiles to JavaScript, but if your published types only work well in a TypeScript project, half your users are left with a worse experience.
Craters ships .d.ts declaration files alongside the compiled output:
dist/
├── craters.js ← compiled output (CommonJS + ESM)
├── craters.d.ts ← type declarations for TS users
└── craters.d.ts.map
Even JavaScript users get autocomplete in VS Code because editors read .d.ts files directly. This matters more than most library authors realise.
This is the part that surprised me most. Moving from JS to TS didn’t just add types to existing code. It found two logic bugs in the component query system that had been silently returning wrong results in edge cases.
The kind of bugs that unit tests miss because they depend on runtime values that tests never actually exercise. TypeScript forces you to be explicit about what a function accepts and returns, and that explicitness forces you to think through edge cases earlier than you otherwise would.
| JavaScript | TypeScript | |
|---|---|---|
| Autocomplete for users | Partial (JSDoc only) | Full |
| Type errors | At runtime | At compile time |
| Refactor a component API | Grep and hope | Compiler tells you everything |
.d.ts declarations |
Manual | Auto-generated |
If you’re maintaining a JS library and considering a TypeScript migration:
allowJs: true — nothing breaks, no forced rewrites yetstrict: true and fix the errors incrementally.ts one at a time, starting from the core outward.d.ts files from day oneDon’t do it all at once. Migrate the internals first, then push types outward to the public surface.
Source: github.com/john-swana/craters