Ember Services are global singleton classes that can be made available to different parts of an Ember application via dependency injection. Due to their global, shared nature, writing services in TypeScript gives you a build-time-enforceable API for some of the most central parts of your application.
A Basic Service
Let's take this example from elsewhere in the Ember Guides:
import Service from '@ember/service';
import { TrackedSet } from 'tracked-built-ins';
export default class ShoppingCartService extends Service {
items = new TrackedSet();
add(item) {
this.items.add(item);
}
remove(item) {
this.items.remove(item);
}
empty() {
this.items.clear();
}
}
Just making this a TypeScript file gives us some type safety without having to add any additional type information. We'll see this when we use the service elsewhere in the application.
Using Services
Ember looks up services with the @service
decorator at runtime, using the name of the service being injected 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 to inject isn't available at compile time in the same way.
Since legacy decorators do not have access to enough information to produce an appropriate type by themselves, we need to import and add the type explicitly. Also, we must use the declare
property modifier to tell the TypeScript compiler to trust that this property will be set up by something outside this component—namely, the decorator. (Learn more about using Ember's decorators with TypeScript here.) Here's an example using the ShoppingCartService
we defined above in a component:
import Component from '@glimmer/component';
import { service } from '@ember/service';
import { action } from '@ember/object';
import ShoppingCartService from 'my-app/services/shopping-cart';
export default class CartContentsComponent extends Component {
@service declare shoppingCart: ShoppingCartService;
@action
remove(item) {
this.shoppingCart.remove(item);
}
}
Any attempt to access a property or method not defined on the service will fail type-checking:
import Component from '@glimmer/component';
import { service } from '@ember/service';
import { action } from '@ember/object';
import ShoppingCartService from 'my-app/services/shopping-cart';
export default class CartContentsComponent extends Component {
@service declare shoppingCart: ShoppingCartService;
@action
remove(item) {
// Error: Property 'saveForLater' does not exist on type 'ShoppingCartService'.
this.shoppingCart.saveForLater(item);
}
}
Services can also be loaded from the dependency injection container manually:
import Component from '@glimmer/component';
import { getOwner } from '@ember/owner';
import { action } from '@ember/object';
export default class CartContentsComponent extends Component {
get cart() {
return getOwner(this)?.lookup('service:shopping-cart');
}
@action
remove(item) {
this.cart.remove(item);
}
}
In order for TypeScript to infer the correct type for the ShoppingCartService
from the call to Owner.lookup
, we must first register the ShoppingCartService
type with declare module
:
export default class ShoppingCartService extends Service {
//...
}
declare module '@ember/service' {
interface Registry {
'shopping-cart': ShoppingCartService;
}
}