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 and Ember
      • TypeScript: Invokables
      • TypeScript: Routing
      • TypeScript: Services
      • TypeScript and EmberData
    • TypeScript: Application Development
    • TypeScript: Additional Resources
  • Developer Tools
  • Ember Inspector
  • Code Editors
  • Additional Resources
  • Upgrading
  • Contributing to Ember.js
  • Glossary

TypeScript: Routing


Routes

Since Ember Routes are just regular JavaScript classes with a few special Ember lifecycle hooks and properties available, TypeScript should "Just Work." Ember's types supply the definitions for the various methods available within route subclasses, which will provide autocomplete and type-checking along the way.

Controllers

Like routes, Controllers are just normal JavaScript classes with a few special Ember lifecycle hooks and properties available.

The main thing to be aware of is special handling around query params. In order to provide type safety for query param configuration, Ember's types specify that when defining a query param's type attribute, you must supply one of the allowed types: 'boolean', 'number', 'array', or 'string' (the default). However, if you supply these types as you would in JS, like this:

app/controllers/my.ts
import Controller from '@ember/controller';

export default class MyController extends Controller {
  queryParams = [
    {
      category: { type: 'array' },
    },
  ];
}

Then you will see a type error like this:

Property 'queryParams' in type 'MyController' is not assignable to the same property in base type 'Controller'.
  Type '{ category: { type: string; }; }[]' is not assignable to type '(string | Record<string, string | QueryParamConfig | undefined>)[]'.
    Type '{ category: { type: string; }; }' is not assignable to type 'string | Record<string, string | QueryParamConfig | undefined>'.
      Type '{ category: { type: string; }; }' is not assignable to type 'Record<string, string | QueryParamConfig | undefined>'.
        Property 'category' is incompatible with index signature.
          Type '{ type: string; }' is not assignable to type 'string | QueryParamConfig | undefined'.
            Type '{ type: string; }' is not assignable to type 'QueryParamConfig'.
              Types of property 'type' are incompatible.
                Type 'string' is not assignable to type '"string" | "number" | "boolean" | "array" | undefined'.ts(2416)

This is because TS currently infers the type of type: "array" as type: string. You can work around this by supplying as const after the declaration:

"app/controllers/my.ts",
import Controller from '@ember/controller';

export default class MyController extends Controller {
  queryParams = [
    {
      category: { type: 'array' },
      category: { type: 'array' as const },
    },
  ];
}

Now it will type-check.

Working with Route Models

We often use routes' models throughout our application, since they're a core ingredient of our application's data. As such, we want to make sure that we have good types for them!

We can start by defining a type utility to let us get the resolved value returned by a route's model hook:

app/lib/type-utils.ts
import type Route from '@ember/routing/route';

/** Get the resolved model value from a route. */
export type ModelFrom<R extends Route> = Awaited<ReturnType<R['model']>>;

How that works:

  • Awaited<P> says "if this is a promise, the type here is whatever the promise resolves to; otherwise, it's just the value"
  • ReturnType<T> gets the return value of a given function
  • R['model'] (where R has to be Route itself or a subclass) says "the property named model on Route R"

ModelFrom<Route> ends up giving you the resolved value returned from the model hook for a given route. We can use this functionality to guarantee that the model on a Controller is always exactly the type returned by Route::model by writing something like this:

app/controllers/controller-with-model.ts
import Controller from '@ember/controller';
import MyRoute from 'my-app/routes/my-route';
import { ModelFrom } from 'my-app/lib/type-utils';

export default class ControllerWithModel extends Controller {
  declare model: ModelFrom<MyRoute>;
}

Now, our controller's model property will always stay in sync with the corresponding route's model hook.

Zoey says...

The ModelFrom type utility only works if you do not mutate the model in either the afterModel or setupController hooks on the route! That's generally considered to be a bad practice anyway.

Controller Injections and Lookups

If you are using controller injections via the @inject decorator from @ember/controller, see the "Decorators" documentation.

If you need to lookup a controller with Owner.lookup, you'll need to first register your controller in Ember's TypeScript Controller registry as described in "Registries":

app/controllers/my.ts
import Controller from '@ember/controller';

export default class MyController extends Controller {
  //...
}

declare module '@ember/controller' {
  interface Registry {
    my: MyController;
  }
}
left arrow
TypeScript: Invokables
TypeScript: Services
right arrow
On this page

  • Routes
  • Controllers
  • Working with Route Models
  • Controller Injections and Lookups
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.