Two things changed that make TypeScript a reasonable proposition in a mature Ember app, and both of them landed quietly enough that a lot of teams have not noticed.
The first is that Ember ships its own types. For years typing an Ember app meant ember-cli-typescript plus a stack of community @types/ember-* packages that drifted from the real API and had to be patched locally. That arrangement is over: the framework packages publish types themselves, and the separate build integration is no longer the thing holding your app together. The second is Glint. Templates used to be the hole in the middle of any Ember TypeScript story — you could type a component class perfectly and still pass the wrong argument to it from a template with no complaint from anything. Glint closes that hole by type-checking templates against the classes and signatures they invoke.
So the question is no longer "is this possible." It is whether it is worth the quarter, and in what order.
What you actually get, stated plainly
The payoff in a long-lived app is not fewer runtime errors. Your app has been in production for a decade; the crash-level bugs are mostly out of it. The payoff is ramp time and refactor confidence.
A new hire reading a typed service knows what it returns without grepping for callers. An engineer renaming a component argument finds every invocation, including the ones in templates, because Glint reads templates as code. When you eventually delete a route during an Octane conversion or a migration increment, the compiler tells you what pointed at it. That is the same benefit a decision log gives you, bought in a different currency, and it compounds for exactly the same reason: the original authors are gone and the code has to explain itself.
If your app is going to be around for another five years, that is worth paying for. If you are twelve months from finishing a migration off Ember, it usually is not — put the types in the new stack instead.
Prerequisites, in order
TypeScript adoption is downstream of work you probably already have on the list. Trying to skip ahead is where teams burn a quarter and give up.
Be on a current LTS. The first-party types arrived recently. On an older line you are back to community type packages, which is the version of this project that fails.
Be substantially Octane. Native classes with decorators type well. Classic components, mixins, and long computed-property chains do not — Mixin in particular has no good type representation, and every mixin in a class you are trying to convert becomes an any-shaped hole that spreads. If you are still mid-conversion, finish the hot paths first. The two projects share most of their work anyway.
Have a test suite you trust. Types will push you to change code, not just annotate it. You want green tests underneath that.
Know your addon situation. Untyped addons are fine — you write a small declaration for what you use. Abandoned addons are the same problem they always are, one more reason to run the annual addon audit.
Turn it on without converting anything
Add TypeScript to the build and configure it permissively: allowJs on, strict off to start, no type errors failing CI. Nothing in the repo changes. Every existing .js file keeps compiling exactly as it did. What you have bought is the ability to write the next file in TypeScript.
Then add Glint in the same posture — running, reporting, not blocking. Expect a large error count on the first run. Do not fix it. Record the number and move on; it is a baseline, not a backlog.
The one thing to decide early is whether CI ratchets. We like a check that fails if the error count goes up, and never mind how high it currently is. It costs an afternoon and it is the difference between an adoption that finishes and one that stalls at 30% two years from now.
Convert in the order the types pay you back
Renaming every file in a week produces a repo full of any and no benefit. Convert where the type actually carries information, roughly in this order:
- Shared utilities and constants. Small, dependency-free, high fan-in. Typing these pushes information outward into every file that imports them, including untyped ones, because the editor picks it up anyway.
- Services. These are your app's real API surface. A typed session, feature-flag, or current-user service is the single highest-value thing in the project.
- Models and the data layer. This is the hard part in an older app, because what the server actually returns and what the model claims are frequently different things, and the types will surface that. Treat the discoveries as findings, not as blockers. Type new request-layer code as you write it rather than retrofitting the whole store at once.
- Leaf components, then containers. Signatures first —
Args,Blocks,Element. This is where Glint starts earning its place: once a component has a signature, every template that invokes it is checked. - Routes and controllers last. Highest coupling, lowest marginal information.
New files are TypeScript from the day you turn it on. Files touched for other reasons get converted if the change is small. That habit alone moves a codebase faster than any scheduled conversion sprint, and it costs nothing in isolation.
Raise strictness on a schedule, not all at once
strict: true on a ten-year-old app produces thousands of errors, most of them about null handling, and a team that stops looking at the output. Enable the flags one at a time, clear each one, then move to the next. noImplicitAny first, strictNullChecks last — it is the one that finds real bugs and also the one that takes the longest.
Use any when you need to, but write it deliberately and comment why. A lie you can grep for is better than a lie spread across four files.
Template types are the part people underestimate
Glint needs component signatures to do its job, so the first stretch feels like writing boilerplate for no return. It turns a corner once the commonly invoked components are done, and then it catches the class of bug that used to reach production: the argument renamed in the class but not in three templates, the block invoked with the wrong number of params, the helper handed a possibly-undefined value.
It also pairs well with the template tag format. In a .gts file the template and the class are in the same module, imports are explicit, and the type checker has everything it needs without indirection. If you are already moving toward template tag, do these together — the combined diff is smaller than two separate passes.
When to wait
We have told clients not to do this. The cases are consistent:
- The app is still largely classic. Convert to Octane first; typing mixins is wasted effort.
- The team has no TypeScript experience and no appetite for it. Half-adopted types are worse than none — they are documentation that lies.
- A migration off Ember is already scheduled and short. Spend the types budget on the destination.
- The app is in genuine maintenance mode, a few small changes a year. There is nothing for the types to protect.
Everywhere else, the sequencing is unremarkable: current LTS, Octane in the hot paths, TypeScript configured loosely, Glint reporting, ratchet in CI, convert services and utilities first, tighten strictness a flag at a time. It runs in the background of normal feature work over two or three quarters, and the app stays shippable the whole way.