home
  • Blog
6.3
  • Introduction
  • Getting Started
  • Tutorial
  • Core Concepts
  • Components
  • Routing
  • Services
  • EmberData
  • In-Depth Topics
  • Application Development
  • Application Concerns
  • Accessibility
  • Configuration
  • Testing
  • Addons and Dependencies
  • Using TypeScript
    • Using TypeScript with Ember
    • TypeScript: Getting Started
    • TypeScript: Core Concepts
    • TypeScript: Application Development
    • TypeScript: Additional Resources
      • Introduction
      • TypeScript: Gotchas and Troubleshooting
      • TypeScript: FAQ and Tips
      • TypeScript: Working with Ember Classic
  • Developer Tools
  • Ember Inspector
  • Code Editors
  • Additional Resources
  • Upgrading
  • Contributing to Ember.js
  • Glossary

TypeScript: Gotchas and Troubleshooting


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:

app/services/shopping-cart.ts
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

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:

app/components/my-game.ts
import Component from '@ember/component';
import { action } from '@ember/object';

export default class MyGame extends Component {
  @action turnWheel(degrees: number) {
    // ...
  }
}
app/components/my-game.hbs
<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:

app/components/args-display.ts
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:

app/routes/my.ts
import Route from '@ember/routing/route';
import Transition from '@ember/routing/transition';

export default class MyRoute extends Route {
  beforeModel(transition: Transition) {
    // ...
  }
}
left arrow
Introduction
TypeScript: FAQ and Tips
right arrow
On this page

  • Registries
  • Decorators
  • Templates
  • Hook Types and Autocomplete
Team Sponsors Security Legal Branding Community Guidelines
Twitter GitHub Discord Mastodon

If you want help you can contact us by email, open an issue, or get realtime help by joining the Ember Discord.

© Copyright 2025 - Tilde Inc.
Ember.js is free, open source and always will be.


Ember is generously supported by
blue Created with Sketch.