Using TypeScript With Ember Effectively
Incremental adoption
If you are porting an existing app to TypeScript, you can install this addon and migrate your files incrementally by changing their extensions from .js
to .ts
. As TypeScript starts to find errors (and it usually does!), make sure to celebrate your wins—even if they're small!—with your team, especially if some people are not convinced yet. We would also love to hear your stories!
Some specific tips for success on the technical front:
First, use the strictest strictness settings that our typings allow (currently all strictness settings except strictFunctionTypes
). While it may be tempting to start with the loosest strictness settings and then to tighten them down as you go, this will actually mean that "getting your app type-checking" will become a repeated process—getting it type-checking with every new strictness setting you enable—rather than something you do just once.
The full recommended strictness settings in your "compilerOptions"
hash (which are also the settings generated by the ember-cli-typescript blueprint):
A good approach is to start at your "leaf" modules (the ones that don't import anything else from your app, only Ember or third-party types) and then work your way back inward toward the most core modules that are used everywhere. Often the highest-value modules are your Ember Data models and any core services that are used everywhere else in the app – and those are also the ones that tend to have the most cascading effects (having to update tons of other places in your app) when you type them later in the process.
Finally, leave "noEmitOnError": true
(the default) in the "compilerOptions"
hash in your tsconfig.json
. This will fail your build if you have type errors, which gives you the fastest feedback as you add types.
What about missing types?
There are two schools of thought on how to handle things you don't have types for as you go:
Liberally use
any
for them and come back and fill them in later. This will let you do the strictest strictness settings but with an escape hatch that lets you say "We will come back to this when we have more idea how to handle it." This approach lets you move faster, but means you will still have lots of runtime type errors:any
just turns the type-checker off for anything touching those modules. You’ll have to come back later and clean those up, and you’ll likely have more difficult refactorings to do at that time.Go more slowly, but write down at least minimally accurate types as you go. (This is easier if you follow the leaves-first strategy recommended above.) This is much slower going, and can feel harder because you can’t just skip over things. Once you complete the work for any given module, though, you can be confident that everything is solid and you won’t have to revisit it in the future.
There is an inherent tradeoff between these two approaches; which works best will depend on your team and your app.
Install other types!
You'll want to use other type definitions as much as possible. The first thing you should do, for example, is install the types for your testing framework of choice: @types/ember-mocha
or @types/ember-qunit
. Beyond that, look for types from other addons: it will mean writing any
a lot less and getting a lot more help both from your editor and from the compiler.
Where can I find types? Some addons will ship them with their packages, and work out of the box. For others, you can search for them on Definitely Typed, or on npm under the @types
namespace. (In the future we hope to maintain a list of known types; keep your eyes open!)
The types
directory
types
directoryDuring installation, we create a types
directory in the root of your application and add a "paths"
mapping that includes that directory in any type lookups TypeScript tries to do. This is convenient for a few things:
global types for your package (see the next section)
writing types for third-party/
vendor
packages which do not have any typesdeveloping types for an addon which you intend to upstream later
These are all fallbacks, of course, you should use the types supplied directly with a package
Global types for your package
At the root of your application or addon, we include a types/<your app>
directory with an index.d.ts
file in it. Anything which is part of your application but which must be declared globally can go in this file. For example, if you have data attached to the Window
object when the page is loaded (for bootstrapping or whatever other reason), this is a good place to declare it.
In the case of applications (but not for addons), we also automatically include declarations for Ember's prototype extensions in this index.d.ts
file, with the Array
prototype extensions enabled and the Function
prototype extensions commented out. You should configure them to match your own config (which we cannot check during installation). If you are disabling Ember's prototype extensions, you can remove these declarations entirely; we include them because they're enabled in most Ember applications today.
Environment configuration typings
Along with the @types/ files mentioned above, ember-cli-typescript adds a starter interface for config/environment.js
in app/config/environment.d.ts
. This interface will likely require some changes to match your app.
We install this file because the actual config/environment.js
is (a) not actually identical with the types as you inherit them in the content of an application, but rather a superset of what an application has access to, and (b) not in a the same location as the path at which you look it up. The actual config/environment.js
file executes in Node during the build, and Ember CLI writes its result as <my-app>/config/environment
into your build for consumption at runtime.
String-keyed lookups
Ember makes heavy use of string-based APIs to allow for a high degree of dynamicism. With some limitations, you can nonetheless use TypeScript very effectively to get auto-complete/IntelliSense as well as to accurately type-check your applications.
A few of the most common speed-bumps are listed here to help make this easier:
Nested keys in get
or set
get
or set
In general, this.get
and this.set
will work as you'd expect if you're doing lookups only a single layer deep. Things like this.get('a.b.c')
don't (and can't ever!) type-check; see the blog posts for a more detailed discussion of why.
The workaround is simply to do one of two things:
The type-safe approach. This will typecheck, but is both ugly and only works *if there are no
null
s orundefined
s along the way. Ifnested
isnull
at runtime, this will crash!Using
// @ts-ignore
. This will not do any type-checking, but is useful for the cases where you are intentionally checking a path which may benull
orundefined
anywhere long it.It's usually best to include an explanation of why you're ignoring a lookup!
Service and controller injections
Ember does service and controller lookups with the inject
functions at runtime, using the name of the service or controller being injected up as the default value—a clever bit of metaprogramming that makes for a nice developer experience. TypeScript cannot do this, because the name of the service or controller to inject isn't available at compile time in the same way.
The officially supported method for injections with TypeScript uses decorators.
Then we can use the service as we usually would with a decorator, but adding a type annotation to it so TypeScript knows what it's looking at:
Note that we need the MySession
type annotation this way, but we don't need the string lookup (unless we're giving the service a different name than the usual on the class, as in Ember injections in general). Without the type annotation, the type of session
would just be any
. This is because decorators are not allowed to modify the types of whatever they decorate. As a result, we wouldn't get any type-checking on that session.login
call, and we wouldn't get any auto-completion either. Which would be really sad and take away a lot of the reason we're using TypeScript in the first place!
Also notice the declare
property modifier. This tells TypeScript that the property will be configured by something outside the class (in this case, the decorator), and guarantees it emits spec-compliant JavaScript.
(This also holds true for all other service injections, computed property macros, and Ember Data model attributes and relationships.)
Earlier Ember versions
A couple notes for consumers on earlier Ember versions:
On Ember versions earlier than 3.1, you'll want to wrap your service type in ComputedProperty
, because native ES5 getters are not available there, which means that instead of accessing the service via this.mySession
, you would have to access it as this.get('mySession')
or get(this, 'mySession')
.
On Ember versions earlier than 3.6, you may encounter problems when providing type definitions like this:
When invoked via a template {{user-profile username='example123'}}
, you would expect that username
would have the value of example123
, however prior to the native class feature released in Ember 3.6
, this will result in username
being undefined.
For users who remain on Ember versions below 3.6
, please use https://github.com/pzuraq/ember-native-class-polyfill
Ember Data lookups
We use the same basic approach for Ember Data type lookups with string keys as we do for service or controller injections. As a result, once you add the module and interface definitions for each model, serializer, and adapter in your app, you will automatically get type-checking and autocompletion and the correct return types for functions like findRecord
, queryRecord
, adapterFor
, serializerFor
, etc. No need to try to write out those (admittedly kind of hairy!) types; just write your Ember Data calls like normal and everything should just work.
The declarations and changes you need to add to your existing files are:
Models
Adapters
Serializers
Transforms
Opt-in unsafety
Also notice that unlike with service and controller injections, there is no unsafe fallback method by default, because there isn't an argument-less variant of the functions to use as there is for Service
and Controller
injection. If for some reason you want to opt out of the full type-safe lookup for the strings you pass into methods like findRecord
, adapterFor
, and serializerFor
, you can add these declarations somewhere in your project:
However, we strongly recommend that you simply take the time to add the few lines of declarations to each of your Model
, Adapter
, and Serializer
instances instead. It will save you time in even the short run!
Fixing the Ember Data error TS2344
problem
error TS2344
problemIf you're developing an Ember app or addon and not using Ember Data (and accordingly not even have the Ember Data types installed), you may see an error like this and be confused:
This happens because the types for Ember's test tooling includes the types for Ember Data because the this
value in several of Ember's test types can include a reference to the Ember Data Store
class.
The fix: add a declaration like this in a new file named ember-data.d.ts
in your types
directory:
This works because (a) we include things in your types directory automatically and (b) TypeScript will merge this module and interface declaration with the main definitions for Ember Data from DefinitelyTyped behind the scenes.
If you're developing an addon and concerned that this might affect consumers, it won't. Your types directory will never be referenced by consumers at all!
Class property setup errors
Some common stumbling blocks for people switching to ES6 classes from the traditional EmberObject setup:
Assertion Failed: InjectedProperties should be defined with the inject computed property macros.
– You've writtensomeService = inject()
in an ES6 class body in Ember 3.1+. Replace it with the.extend
approach or by using decorators(@service
or@controller
) as discussed above. Because computed properties of all sorts, including injections, must be set up on a prototype, not on an instance, if you try to use class properties to set up injections, computed properties, the action hash, and so on, you will see this error.Assertion Failed: Attempting to lookup an injected property on an object without a container, ensure that the object was instantiated via a container.
– You failed to pass...arguments
when you calledsuper
in e.g. a component classconstructor
. Always dosuper(...arguments)
, not justsuper()
, in yourconstructor
.
Type definitions outside node_modules/@types
node_modules/@types
By default, the TypeScript compiler loads all type definitions found in node_modules/@types
. If the type defs you need are not found there and are not supplied in the root of the package you're referencing, you can register a custom value in paths
in the tsconfig.json
file. See the tsconfig.json docs for details.
Last updated