This section covers the common details and "gotchas" of using TypeScript with Ember.
Registries
Ember makes heavy use of string-based APIs to allow for a high degree of dynamicness. With some limitations, you can nonetheless use TypeScript very effectively to get auto-complete/IntelliSense as well as to accurately type-check your applications by using registries.
Here's an example defining a Shopping Cart Service in the Ember Service registry:
export default class ShoppingCartService extends Service {
//...
}
declare module '@ember/service' {
interface Registry {
'shopping-cart': ShoppingCartService;
}
}
This registry definition allows for type-safe lookups in string-based APIs. For example, the Owner.lookup
method uses this "registration"—a mapping from the string 'shopping-cart'
to the service type, ShoppingCartService
—to provide the correct type:
import type Owner from '@ember/owner';
function dynamicLookup(owner: Owner) {
let cart = owner.lookup('service:shopping-cart');
cart.add('hamster feed');
}
For examples, see:
- Service registry
- Controller registry
- EmberData Model registry
- EmberData Transform registry
- EmberData Serializer registry
- EmberData Adapter registry
Decorators
Ember makes heavy use of decorators, and TypeScript does not support deriving type information from Ember's legacy decorators.
As a result, whenever using a decorator to declare a class field the framework sets up for you, you should mark it with declare
. That includes all service injections (@service
), controller injections (@inject
) as well as all EmberData attributes (@attr
) and relationships (@belongsTo
and @hasMany
).
Normally, TypeScript
determines whether a property is definitely not null
or undefined
by checking what you do in the constructor. In the case of legacy decorators, though, TypeScript does not have visibility into how the decorated properties are initialized. The declare
annotation informs TypeScript that a declaration is defined somewhere else, outside its scope.
Additionally, you are responsible to write the type correctly. TypeScript does not use legacy decorator information at all in its type information. If you write @service foo
or even @service('foo') foo
, Ember knows that this resolves at runtime to the service Foo
, but TypeScript does not and—for now—cannot.
This means that you are responsible to provide this type information, and that you are responsible to make sure that the information remains correct and up-to-date.
For examples, see:
@service
@inject
- EmberData
@attr
- EmberData
@belongsTo
- EmberData
@hasMany
Templates
Templates are currently totally non-type-checked. This means that you lose any safety when moving into a template context, even if using a Glimmer Component
in Ember Octane. (Looking for type-checking in templates? Try Glint!)
For example, TypeScript won't detect a mismatch between this action and the corresponding call in the template:
import Component from '@ember/component';
import { action } from '@ember/object';
export default class MyGame extends Component {
@action turnWheel(degrees: number) {
// ...
}
}
<button {{on 'click' (fn this.turnWheel 'potato')}}>
Click Me
</button>
Hook Types and Autocomplete
Let's imagine a component which just logs the names of its arguments when it is first constructed. First, we must define the Signature and pass it into our component, then we can use the Args
member in our Signature to set the type of args
in the constructor:
import Component from '@glimmer/component';
const log = console.log.bind(console);
export interface ArgsDisplaySignature {
Args: {
arg1: string;
arg2: number;
arg3: boolean;
};
}
export default class ArgsDisplay extends Component<ArgsDisplaySignature> {
constructor(owner: unknown, args: ArgsDisplaySignature['Args']) {
super(owner, args);
Object.keys(args).forEach(log);
}
}
Notice that we have to start by calling super
with owner
and args
. This may be a bit different from what you're used to in Ember or other frameworks, but is normal for sub-classes in TypeScript today. If the compiler just accepted any ...arguments
, a lot of potentially very unsafe invocations would go through. So, instead of using ...arguments
, we explicitly pass the specific arguments and make sure their types match up with what the super-class expects.
The types for owner
here and args
line up with what the constructor
for Glimmer components expects. The owner
is specified as unknown
because this is a detail we explicitly don't need to know about. The args
are the Args
from the Signature we defined.
Additionally, the types of the arguments passed to subclassed methods will not autocomplete as you may expect. This is because in JavaScript, a subclass may legally override a superclass method to accept different arguments. Ember's lifecycle hooks, however, are called by the framework itself, and thus the arguments and return type should always match the superclass. Unfortunately, TypeScript does not and cannot know that, so we have to provide the types directly.
Accordingly, we have to provide the types for hooks ourselves:
import Route from '@ember/routing/route';
import Transition from '@ember/routing/transition';
export default class MyRoute extends Route {
beforeModel(transition: Transition) {
// ...
}
}
Fixing the EmberData error TS2344
problem
If you're developing an Ember app or addon and not using EmberData (and accordingly not even have the EmberData types installed), you may see an error like this and be confused:
node_modules/@types/ember-data/index.d.ts(920,56): error TS2344: Type 'any' does not satisfy the constraint 'never'.
This happens because the types for Ember's test tooling includes the types for EmberData because the this
value in several of Ember's test types can include a reference to the EmberData Store
class.
The fix: add a declaration like this in a new file named ember-data.d.ts
in your types
directory:
declare module 'ember-data/types/registries/model' {
export default interface ModelRegistry {
[key: string]: unknown;
}
}
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 EmberData 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!