ember-cli-typescript
  • ember-cli-typescript
  • Installation
  • Configuration
  • TypeScript and Ember
    • Using TypeScript With Ember Effectively
    • Decorators
    • Current limitations
    • Building Addons in TypeScript
    • Understanding the @types Package Names
  • Working With Ember
    • Components
    • Services
    • Routes
    • Controllers
    • Helpers
    • Testing
  • Working With Ember Data
    • Models
    • Transforms
  • Cookbook
    • Working with route models
  • Working With Ember Classic
    • EmberComponent
    • Mixins
    • Computed Properties
    • EmberObject
  • Upgrading from 1.x
  • Troubleshooting
    • Conflicting Type Dependencies
Powered by GitBook
On this page
  • A basic service
  • Using services

Was this helpful?

Edit on GitHub
Export as PDF
  1. Working With Ember

Services

PreviousComponentsNextRoutes

Last updated 2 years ago

Was this helpful?

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-enforcable API for some of the most central parts of your application.

If you are not familiar with Services in Ember, first make sure you have read and understood the !

A basic service

Let's take this example from the :

import { A } from '@ember/array';
import Service from '@ember/service';

export default class ShoppingCartService extends Service {
  items = A([]);

  add(item) {
    this.items.pushObject(item);
  }

  remove(item) {
    this.items.removeObject(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.

import { TrackedArray } from 'tracked-built-ins';
import Service from '@ember/service';

export default class ShoppingCartService extends Service {
  items = new TrackedArray();

  add(item) {
    this.items.push(item);
  }

  remove(item) {
    this.items.splice(1, this.items.findIndex((i) => i === item));
  }

  empty() {
    this.items.clear();
  }
}

Notice that here we are using only built-in array operations, not Ember's custom array methods.

Using services

You can use a service in any container-resolved object such as a component or another service. Services are injected into these objects by decorating a property with the inject decorator. Because decorators can't affect the type of the property they decorate, we must manually type the property. Also, we must use declare modifier to tell the TypeScript compiler to trust that this property will be set up by something outside this component—namely, the decorator.

Here's an example of using the ShoppingCartService we defined above in a component:

import Component from '@glimmer/component';
import { inject as 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 { inject as 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';

import ShoppingCartService from 'my-app/services/shopping-cart';

export default class CartContentsComponent extends Component {
  get cart() {
    return getOwner(this)?.lookup('service:shopping-cart') as ShoppingCartService;
  }

  @action
  remove(item) {
    this.cart.remove(item);
  }
}

Here we need to cast the lookup result to ShoppingCartService in order to get any type-safety because the lookup return type is any (see caution below).

This type-cast provides no guarantees that what is returned by the lookup is actually the service you are expecting. Because TypeScript cannot resolve the lookup micro-syntax (service:<name>) to the service class, a typo would result in returning something other than the specified type. It only gurantees that if the expected service is returned that you are using it correctly.

For now, however, remember that the cast is unsafe!

When working in Octane, you're better off using a TrackedArray from instead of the classic EmberArray:

There is a merged (but not yet implemented) which improves this design and makes it straightforward to type-check. Additionally, TypeScript 4.1's introduction of may allow us to supply types that work with the microsyntax.

Ember Guide on Services
Ember Guide
tracked-built-ins
RFC
template types