Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
This guide covers the common details and "gotchas" of using TypeScript with Ember. Note that we do not cover the use of TypeScript or Ember in general—for those, you should refer to the corresponding documentation:
You can simply ember install
the dependency like normal:
All dependencies will be added to your package.json
, and you're ready to roll!
If you're upgrading from a previous release, see (./upgrade-notes.md).
Installing ember-cli-typescript modifies your project in two ways:
installing a number of other packages to make TypeScript work in your app or addon
generating a number of files in your project
We install all of the following packages at their current "latest" value, :
typescript
ember-cli-typescript-blueprints
@types/ember
@types/ember-data
@types/ember__*
– @types/ember__object
for @ember/object
etc.
@types/ember-data__*
– @types/ember-data__model
for @ember-data/model
etc.
@types/rsvp
We also add the following files to your project:
types/<app name>/index.d.ts
– the location for any global type declarations you need to write for you own application; see Using TS Effectively: Global types for your package for information on its default contents and how to use it effectively
app/config/environment.d.ts
– a basic set of types defined for the contents of the config/environment.js
file in your app; see Environment and configuration typings for details
Ember makes heavy use of decorators, and TypeScript does not currently support deriving type information from decorators.
As a result, there are three important points that apply to all decorator usage in Ember:
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 and controller injections as well as all Ember Data attributes and relationships.
Normally, TypeScript determines whether a property is definitely not null
or undefined
by checking what you do in the constructor. In the case of service injections, controller injections, or Ember Data model decorations, though, TypeScript does not have visibility into how instances of the class are initialized. The declare
annotation informs TypeScript that a declaration is defined somewhere else, outside its scope.
For Ember Data Models, you will need to use the optional ?
operator on field declarations if the field is optional (?
). See the Ember Data section of the guide for more details!
You are responsible to write the type correctly. TypeScript does not currently use 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 the detailed discussions of the two main places decorators are used in the framework:
If you are porting an existing app to TypeScript, you can install this addon and migrate your files incrementally by changing their extensions from .js
to .ts
. As TypeScript starts to find errors (and it usually does!), make sure to celebrate your wins—even if they're small!—with your team, especially if some people are not convinced yet. We would also love to hear your stories!
Some specific tips for success on the technical front:
First, use the strictest strictness settings that our typings allow (currently all strictness settings except strictFunctionTypes
). While it may be tempting to start with the loosest strictness settings and then to tighten them down as you go, this will actually mean that "getting your app type-checking" will become a repeated process—getting it type-checking with every new strictness setting you enable—rather than something you do just once.
The full recommended strictness settings in your "compilerOptions"
hash (which are also the settings generated by the ember-cli-typescript blueprint):
A good approach is to start at your "leaf" modules (the ones that don't import anything else from your app, only Ember or third-party types) and then work your way back inward toward the most core modules that are used everywhere. Often the highest-value modules are your Ember Data models and any core services that are used everywhere else in the app – and those are also the ones that tend to have the most cascading effects (having to update tons of other places in your app) when you type them later in the process.
Finally, leave "noEmitOnError": true
(the default) in the "compilerOptions"
hash in your tsconfig.json
. This will fail your build if you have type errors, which gives you the fastest feedback as you add types.
There are two schools of thought on how to handle things you don't have types for as you go:
Liberally use any
for them and come back and fill them in later. This will let you do the strictest strictness settings but with an escape hatch that lets you say "We will come back to this when we have more idea how to handle it." This approach lets you move faster, but means you will still have lots of runtime type errors: any
just turns the type-checker off for anything touching those modules. You’ll have to come back later and clean those up, and you’ll likely have more difficult refactorings to do at that time.
Go more slowly, but write down at least minimally accurate types as you go. (This is easier if you follow the leaves-first strategy recommended above.) This is much slower going, and can feel harder because you can’t just skip over things. Once you complete the work for any given module, though, you can be confident that everything is solid and you won’t have to revisit it in the future.
There is an inherent tradeoff between these two approaches; which works best will depend on your team and your app.
You'll want to use other type definitions as much as possible. The first thing you should do, for example, is install the types for your testing framework of choice: @types/ember-mocha
or @types/ember-qunit
. Beyond that, look for types from other addons: it will mean writing any
a lot less and getting a lot more help both from your editor and from the compiler.
Where can I find types? Some addons will ship them with their packages, and work out of the box. For others, you can search for them on Definitely Typed, or on npm under the @types
namespace. (In the future we hope to maintain a list of known types; keep your eyes open!)
types
directoryDuring installation, we create a types
directory in the root of your application and add a "paths"
mapping that includes that directory in any type lookups TypeScript tries to do. This is convenient for a few things:
global types for your package (see the next section)
writing types for third-party/vendor
packages which do not have any types
developing types for an addon which you intend to upstream later
These are all fallbacks, of course, you should use the types supplied directly with a package
At the root of your application or addon, we include a types/<your app>
directory with an index.d.ts
file in it. Anything which is part of your application but which must be declared globally can go in this file. For example, if you have data attached to the Window
object when the page is loaded (for bootstrapping or whatever other reason), this is a good place to declare it.
In the case of applications (but not for addons), we also automatically include declarations for Ember's prototype extensions in this index.d.ts
file, with the Array
prototype extensions enabled and the Function
prototype extensions commented out. You should configure them to match your own config (which we cannot check during installation). If you are disabling Ember's prototype extensions, you can remove these declarations entirely; we include them because they're enabled in most Ember applications today.
Along with the @types/ files mentioned above, ember-cli-typescript adds a starter interface for config/environment.js
in app/config/environment.d.ts
. This interface will likely require some changes to match your app.
We install this file because the actual config/environment.js
is (a) not actually identical with the types as you inherit them in the content of an application, but rather a superset of what an application has access to, and (b) not in a the same location as the path at which you look it up. The actual config/environment.js
file executes in Node during the build, and Ember CLI writes its result as <my-app>/config/environment
into your build for consumption at runtime.
Ember makes heavy use of string-based APIs to allow for a high degree of dynamicism. With some limitations, you can nonetheless use TypeScript very effectively to get auto-complete/IntelliSense as well as to accurately type-check your applications.
A few of the most common speed-bumps are listed here to help make this easier:
get
or set
In general, this.get
and this.set
will work as you'd expect if you're doing lookups only a single layer deep. Things like this.get('a.b.c')
don't (and can't ever!) type-check; see the blog posts for a more detailed discussion of why.
The workaround is simply to do one of two things:
The type-safe approach. This will typecheck, but is both ugly and only works *if there are no null
s or undefined
s along the way. If nested
is null
at runtime, this will crash!
Using // @ts-ignore
. This will not do any type-checking, but is useful for the cases where you are intentionally checking a path which may be null
or undefined
anywhere long it.
It's usually best to include an explanation of why you're ignoring a lookup!
Ember does service and controller lookups with the inject
functions at runtime, using the name of the service or controller being injected up 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 or controller to inject isn't available at compile time in the same way.
The officially supported method for injections with TypeScript uses decorators.
Then we can use the service as we usually would with a decorator, but adding a type annotation to it so TypeScript knows what it's looking at:
Note that we need the MySession
type annotation this way, but we don't need the string lookup (unless we're giving the service a different name than the usual on the class, as in Ember injections in general). Without the type annotation, the type of session
would just be any
. This is because decorators are not allowed to modify the types of whatever they decorate. As a result, we wouldn't get any type-checking on that session.login
call, and we wouldn't get any auto-completion either. Which would be really sad and take away a lot of the reason we're using TypeScript in the first place!
Also notice the declare
property modifier. This tells TypeScript that the property will be configured by something outside the class (in this case, the decorator), and guarantees it emits spec-compliant JavaScript.
(This also holds true for all other service injections, computed property macros, and Ember Data model attributes and relationships.)
A couple notes for consumers on earlier Ember versions:
On Ember versions earlier than 3.1, you'll want to wrap your service type in ComputedProperty
, because native ES5 getters are not available there, which means that instead of accessing the service via this.mySession
, you would have to access it as this.get('mySession')
or get(this, 'mySession')
.
On Ember versions earlier than 3.6, you may encounter problems when providing type definitions like this:
When invoked via a template {{user-profile username='example123'}}
, you would expect that username
would have the value of example123
, however prior to the native class feature released in Ember 3.6
, this will result in username
being undefined.
For users who remain on Ember versions below 3.6
, please use https://github.com/pzuraq/ember-native-class-polyfill
We use the same basic approach for Ember Data type lookups with string keys as we do for service or controller injections. As a result, once you add the module and interface definitions for each model, serializer, and adapter in your app, you will automatically get type-checking and autocompletion and the correct return types for functions like findRecord
, queryRecord
, adapterFor
, serializerFor
, etc. No need to try to write out those (admittedly kind of hairy!) types; just write your Ember Data calls like normal and everything should just work.
The declarations and changes you need to add to your existing files are:
Models
Adapters
Serializers
Transforms
Also notice that unlike with service and controller injections, there is no unsafe fallback method by default, because there isn't an argument-less variant of the functions to use as there is for Service
and Controller
injection. If for some reason you want to opt out of the full type-safe lookup for the strings you pass into methods like findRecord
, adapterFor
, and serializerFor
, you can add these declarations somewhere in your project:
However, we strongly recommend that you simply take the time to add the few lines of declarations to each of your Model
, Adapter
, and Serializer
instances instead. It will save you time in even the short run!
error TS2344
problemIf you're developing an Ember app or addon and not using Ember Data (and accordingly not even have the Ember Data types installed), you may see an error like this and be confused:
This happens because the types for Ember's test tooling includes the types for Ember Data because the this
value in several of Ember's test types can include a reference to the Ember Data Store
class.
The fix: add a declaration like this in a new file named ember-data.d.ts
in your types
directory:
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 Ember Data 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!
Some common stumbling blocks for people switching to ES6 classes from the traditional EmberObject setup:
Assertion Failed: InjectedProperties should be defined with the inject computed property macros.
– You've written someService = inject()
in an ES6 class body in Ember 3.1+. Replace it with the .extend
approach or by using decorators(@service
or @controller
) as discussed above. Because computed properties of all sorts, including injections, must be set up on a prototype, not on an instance, if you try to use class properties to set up injections, computed properties, the action hash, and so on, you will see this error.
Assertion Failed: Attempting to lookup an injected property on an object without a container, ensure that the object was instantiated via a container.
– You failed to pass ...arguments
when you called super
in e.g. a component class constructor
. Always do super(...arguments)
, not just super()
, in your constructor
.
node_modules/@types
By default, the TypeScript compiler loads all type definitions found in node_modules/@types
. If the type defs you need are not found there and are not supplied in the root of the package you're referencing, you can register a custom value in paths
in the tsconfig.json
file. See the tsconfig.json docs for details.
tsconfig.json
However, there are a few things worth noting if you're already familiar with TypeScript and looking to make further or more advanced customizations (but most users can just ignore this section!):
The generated tsconfig file does not set "outDir"
and sets "noEmit"
to true
. The default configuration we generate allows you to run editors which use the compiler without creating extraneous .js
files throughout your codebase, leaving the compilation to ember-cli-typescript to manage.
Closely related to the previous point: any changes you do make to outDir
won't have any effect on how Ember builds your application—we run the entire build pipeline through Babel's TypeScript support instead of through the TypeScript compiler.
Since your application is built by Babel, and only type-checked by TypeScript, we set the target
key in tsconfig.json
to the current version of the ECMAScript standard so that type-checking uses the latest and greatest from the JavaScript standard library. The Babel configuration in your app's config/targets.js
and any included polyfills will determine the final build output.
To enable TypeScript sourcemaps, you'll need to add the corresponding configuration for Babel to your ember-cli-build.js
file:
(Note that this will noticeably slow down your app rebuilds.)
This guide is designed to help you get up and running with TypeScript in an Ember app.
What is TypeScript, and why should you adopt it?
TypeScript lets you build ambitious web applications with confidence—so it’s a perfect fit for Ember apps!
Get rid of undefined is not a function
and null is not an object
once and for all.
Enjoy API docs… that are always up-to-date.
Experience better developer productivity through top-notch editor support, including incredible autocomplete, guided refactorings, automatic imports, and more.
See also:
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.
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.
Notice that here we are using only built-in array operations, not Ember's custom array methods.
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:
Any attempt to access a property or method not defined on the service will fail type-checking:
Services can also be loaded from the dependency injection container manually:
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!
Working with Routes is in general just working normal TypeScript classes. Ember's types supply the definitions for the various lifecycle events available within route subclasses, which will provide autocomplete and type-checking along the way in general.
However, there is one thing to watch out for: the types of the arguments passed to methods will not autocomplete as you may expect. This is because in general a subclass may override a superclass method as long as it calls its superclass's method correctly. This is very bad practice, but it is legal JavaScript! This is never a concern for lifecycle hooks in Ember, because they are called by the framework itself. However, TypeScript does not and cannot know that, so we have to provide the types directly.
Accordingly, and because the Transition
type is not currently exported as a public type, you may find it convenient to define it using TypeScript's ReturnType
utility type, which does exactly what it sounds like and gives us a local type which is the type returned by some function. The RouterService.transitionTo
returns a Transition
, so we can rely on that as stable public API to define Transition
locally ourselves:
The Resolved<T>
utility type takes in any type, and if the type is a Promise
it transforms the type into whatever the Promise
resolves to; otherwise it just returns the same type. (If you’re using TypeScript 4.5 or later, you can use the built-in Awaited<T>
type, which does the same thing but more robustly: it also handles nested promises.) As we saw above, ReturnType
gets us the return type of the function. So our final MyRouteModel
type takes the return type from our model
hook, and uses the Resolved
type to get the type the promise will resolve to—that is, exactly the type we will have available as @model
in the template and as this.model
on a controller.
This in turn allows us to use the route class to define the type of the model on an associated controller.
Notice here that the model
is declared as optional. That’s intentional: the model
for a given controller is not set when the controller is constructed (that actually happens either when the page corresponding to the controller is created or the first time a <LinkTo>
which links to that page is rendered). Instead, the model
is set on the controller when the corresponding route is successfully entered, via its setupController
hook.
The main thing you need 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:
Then you will see a type error like this:
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:
Now it will type-check.
Building addons in TypeScript offers many of the same benefits as building apps that way: it puts an extra tool at your disposal to help document your code and ensure its correctness. For addons, though, there's one additional bonus: publishing type information for your addons enables autocomplete and inline documentation for your consumers, even if they're not using TypeScript themselves.
Like other addons that preprocess source files, ember-cli-typescript
must be in your addon's dependencies
, not devDependencies
.
Because addons have no control over how files in app/
are transpiled, you cannot have .ts
files in your addon's app/
folder.
When you publish an addon written in TypeScript, the .ts
files will be consumed and transpiled by Babel as part of building the host application the same way .js
files are, in order to meet the requirements of the application's config/targets.js
. This means that no special steps are required for your source code to be consumed by users of your addon.
Even though you publish the source .ts
files, though, by default you consumers who also use TypeScript won't be able to benefit from those types, because the TS compiler isn't aware of how ember-cli
resolves import paths for addon files. For instance, if you write import { foo } from 'my-addon/bar';
, the typechecker has no way to know that the actual file on disk for that import path is at my-addon/addon/bar.ts
.
In order for your addon's users to benefit from type information from your addon, you need to put .d.ts
declaration files at the location on disk where the compiler expects to find them. This addon provides two commands to help with that: ember ts:precompile
and ember ts:clean
. The default ember-cli-typescript
blueprint will configure your package.json
to run these commands in the prepack
and postpack
phases respectively, but you can also run them by hand to verify that the output looks as you expect.
The ts:precompile
command will populate the overall structure of your package with .d.ts
files laid out to match their import paths. For example, addon/index.ts
would produce an index.d.ts
file in the root of your package.
The ts:clean
command will remove the generated .d.ts
files, leaving your working directory back in a pristine state.
The TypeScript compiler has very particular rules when generating declaration files to avoid letting private types leak out unintentionally. You may find it useful to run ember ts:precompile
yourself as you're getting a feel for these rules to ensure everything will go smoothly when you publish.
When you do this for a TypeScript addon, the source files will be picked up in the host app build and everything will execute at runtime as you'd expect. If the host app is also using TypeScript, though, it won't be able to resolve imports from your addon by default, for the reasons outlined above in the Publishing section.
You could run ember ts:precompile
in your addon any time you change a file, but for development a simpler option is to temporarily update the paths
configuration in the host application so that it knows how to resolve types from your linked addon.
Add entries for <addon-name>
and <addon-name>/*
in your tsconfig.json
like so:
One difference as compared to regular published addons: you know whether or not the host app is using ember-cli-typescript
, and if it is, you can safely put .ts
files in an in-repo addon's app/
folder.
A very simple Glimmer component which lets you change the count of a value might look like this:
Notice that there are no type declarations here – but this is actually a well-typed component. The type of count
is number
, and if we accidentally wrote something like this.count = "hello"
the compiler would give us an error.
So far so good, but of course most components aren’t quite this simple! Instead, they’re invoked by other templates and they can invoke other components themselves in their own templates.
Glimmer components can receive both arguments and attributes when they are invoked. When you are working with a component’s backing class, you have access to the arguments but not to the attributes. The arguments are passed to the constructor, and then available as this.args
on the component instance afterward.
Args
represents the arguments your component accepts. Typically this will be an object type mapping the names of your args to their expected type. For example:
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:
If you’re used to the classic Ember Object model, there are two important differences in the constructor itself:
we use super
instead of this._super
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 expect. 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.
args
Now, looking at that bit of code, you might be wondering how it knows what the type of this.args
is. In the constructor
version, we explicitly named the type of the args
argument. Here, it seems to just work automatically. This works because the type definition for a Glimmer component looks roughly like this:
The type signature for Component, with Args extends {} = {}
, means that the component always has a property named args
—
with the type Args
which can be anything that extends the type {}
– an object
and defaults to being just an empty object – = {}
This is analogous to the type of Array
: since you can have an array of string
, or an array of number
or an array of SomeFancyObject
, the type of array is Array<T>
, where T
is the type of thing in the array, which TypeScript normally figures out for you automatically at compile time:
In the case of the Component, we have the types the way we do so that you can’t accidentally define args
as a string, or undefined
, or whatever: it has to be an object. Thus, Component<Args extends {}>
. But we also want to make it so that you can just write extends Component
, so that needs to have a default value. Thus, Component<Args extends {} = {}>
.
args
a typeNow let’s put this to use. Imagine we’re constructing a user profile component which displays the user’s name and optionally an avatar and bio. The template might look something like this:
Then we could capture the types for the profile with an interface representing the arguments:
Assuming the default tsconfig.json
settings (with strictNullChecks: true
), this wouldn't type-check if we didn't check whether the bio
argument were set.
If you'd like to make your own component subclass-able, you need to make it generic as well.
Are you sure you want to provide an inheritance-based API? Oftentimes, it's easier to maintain (and involves less TypeScript hoop-jumping) to use a compositional API instead. If you're sure, here's how!
Requiring that Args extends FancyInputArgs
means that subclasses can have more than these args, but not fewer. Specifying that the Args = FancyInputArgs
means that they default to just being FancyInputArgs
, so users don't need to supply an explicit generic type parameter here unless they're adding more arguments to the class.
We generate a good default , which will usually make everything Just Work™. In general, you may customize your TypeScript build process as usual using the tsconfig.json
file.
You can still customize those properties in tsconfig.json
if your use case requires it, however. For example, to see the output of the compilation in a separate folder you are welcome to set "outDir"
to some path and set "noEmit"
to false
. Then tools which use the TypeScript compiler (e.g. the watcher tooling in JetBrains IDEs) will generate files at that location, while the Ember.js/ pipeline will continue to use its own temp folder.
If you make changes to the paths included in or excluded from the build via your tsconfig.json
(using the "include"
, "exclude"
, or "files"
keys), you will need to restart the server to take the changes into account: ember-cli-typescript does not currently watch the tsconfig.json
file. For more details, see .
If you are using , you might need to include in your webpack configuration:
If you're updating from an older version of the addon, you may also need to update your tsconfig.json
. (Current versions generate the correct config at installation.) Either run ember generate ember-cli-typescript
or verify you have the same sourcemap settings in your tscsonfig.json
that appear in .
This documentation is now hosted on the ember guides website here:
This is not an introduction to TypeScript or Ember. Throughout this guide, we’ll link back to and when there are specific concepts that we will not explain here but which are important for understanding what we’re covering!
To get started, check out the instructions in
If you're totally new to using TypeScript with Ember, start with .
Once you have a good handle on the basics, you can dive into the guides to working with the APIs specific to and .
If you're working with legacy (pre-Octane) Ember and TypeScript together, you should read .
Looking for type-checking in Glimmer templates? Check out .
TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. —
You may be wondering why the packages added to your package.json
and described in are named things like @types/ember__object
instead of something like @types/@ember/object
. This is a conventional name used to allow both the compiler and the publishing infrastructure () to handle scoped packages, documented under in .
If you are not familiar with Services in Ember, first make sure you have read and understood the !
Let's take this example from the :
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.
We do not cover general usage of Ember; instead, we assume that as background knowledge. Please see the Ember and !
This inconsistency will be solved in the future. For now, this workaround gets the job done, and also shows the way to using this information to provide the type of the route's model to other consumers: see for details!
Like , controllers are just normal classes with a few special Ember lifecycle hooks and properties available.
To process .ts
files, ember-cli-typescript
tells Ember CLI to so that Babel knows how to strip away TypeScript-specific syntax. This means that ember-cli-typescript
operates according to the same set of rules as other preprocessors when used by other addons.
Often when developing an addon, it can be useful to run that addon in the context of some other host app so you can make sure it will integrate the way you expect, e.g. using or .
work in much the same way as linked ones. Their .ts
files are managed automatically by ember-cli-typescript
in their dependencies
, and you can ensure imports resolve correctly from the host by adding entries in paths
in the base tsconfig.json
file.
New to Ember or the Octane edition specifically? You may want to read first!
Glimmer Components are defined in one of three ways: with templates only, with a template and a backing class, or with only a backing class (i.e. a yield
-only component). When using a backing class, you get a first-class experience using TypeScript! For type-checking Glimmer templates as well, see .
Since the implementation of , Glimmer and Ember components accept a Signature
type parameter as part of their definition. This parameter is expected to be an object type with (up to) three members: Args
, Element
and Blocks
.
If no Args
key is specified, it will be a type error to pass any arguments to your component. You can read more about Element
and Block
in the Glint .
we must call super
before we do anything else with this
, because in a subclass this
is set up by running the superclass's constructor first (as implied by )
This might change in the future! If TypeScript eventually adds , using ...arguments
could become safe.
The args
passed to a Glimmer Component , so we could change our definition to return the names of the arguments from a getter:
Not sure what’s up with <Args>
at all? We highly recommend the book’s to be quite helpful in understanding this part.
Helpers in Ember are just functions or classes with a well-defined interface, which means they largely Just Work™ with TypeScript. However, there are a couple things you’ll want to watch out for.
As always, you should start by reading and understanding the Ember Guide on Helpers!
The basic type of a helper function in Ember is:
This represents a function which may have an arbitrarily-long list of positional arguments, which may be followed by a single dictionary-style object containing any named arguments.
There are three important points about this definition:
positional
is an array of unknown
, of unspecified length.
named
is a Record
.
Both arguments are always set, but may be empty.
Let’s walk through each of these.
positional
argumentsThe type is an array of unknown
because we don’t (yet!) have any way to make templates aware of the information in this definition—so users could pass in anything. We can work around this using type narrowing—TypeScript’s way of using runtime checks to inform the types at runtime.
named
argumentsWe specified the type of named
as a Record<string, unknown>
. Record
is a built-in TypeScript type representing a fairly standard type in JavaScript: an object being used as a simple map of keys to values. Here we set the values to unknown
and the keys to string
, since that accurately represents what callers may actually pass to a helper.
(As with positional
, we specify the type here as unknown
to account for the fact that the template layer isn’t aware of types yet.)
positional
and named
presenceNote that even if the user passes no arguments, both positional
and named
are always present. They will just be empty in that case. For example:
Given those constraints, let’s see what a (very contrived) actual helper might look like in practice. Let’s imagine we want to take a pair of strings and join them with a required separator and optional prefix and postfixes:
The basic type of a class-based helper function in Ember is:
Notice that the signature of compute
is the same as the signature for the function-based helper! This means that everything we said above applies in exactly the same way here. The only differences are that we can have local state and, by extending from Ember’s Helper
class, we can hook into the dependency injection system and use services.
For more details on using decorators, see our guide to using decorators. For details on using services, see our guide to services.
In Ember Data, attr
defines an attribute on a Model. By default, attributes are passed through as-is, however you can specify an optional type to have the value automatically transformed. Ember Data ships with four basic transform types: string
, number
, boolean
and date
.
You can define your own transforms by subclassing Transform. Ember Data transforms are normal TypeScript classes. The return type of deserialize
method becomes type of the model class property.
You may define your own transforms in TypeScript like so:
Note that you should declare your own transform under TransformRegistry
to make attr
to work with your transform.
Ember Data models are normal TypeScript classes, but with properties decorated to define how the model represents an API resource and relationships to other resources. The decorators the library supplies "just work" with TypeScript at runtime, but require type annotations to be useful with TypeScript.
For details about decorator usage, see our overview of how Ember's decorators work with TypeScript.
@attr
The type returned by the @attr
decorator is whatever Transform is applied via the invocation. See our overview of Transforms for more information.
If you supply no argument to @attr
, the value is passed through without transformation.
If you supply one of the built-in transforms, you will get back a corresponding type:
@attr('string')
→ string
@attr('number')
→ number
@attr('boolean')
→ boolean
@attr('date')
→ Date
If you supply a custom transform, you will get back the type returned by your transform.
So, for example, you might write a class like this:
Very important: Even more than with decorators in general, you should be careful when deciding whether to mark a property as optional ?
or definitely present (no annotation): Ember Data will default to leaving a property empty if it is not supplied by the API or by a developer when creating it. That is: the default for Ember corresponds to an optional field on the model.
The safest type you can write for an Ember Data model, therefore, leaves every property optional: this is how models actually behave. If you choose to mark properties as definitely present by leaving off the ?
, you should take care to guarantee that this is a guarantee your API upholds, and that ever time you create a record from within the app, you uphold those guarantees.
One way to make this safer is to supply a default value using the defaultValue
on the options hash for the attribute:
Relationships between models in Ember Data rely on importing the related models, like import User from './user';
. This, naturally, can cause a recursive loop, as /app/models/post.ts
imports User
from /app/models/user.ts
, and /app/models/user.ts
imports Post
from /app/models/post.ts
. Recursive importing triggers an import/no-cycle
error from eslint.
To avoid these errors, use type-only imports, available since TypeScript 3.8:
@belongsTo
The type returned by the @belongsTo
decorator depends on whether the relationship is { async: true }
(which it is by default).
If the value is true
, the type you should use is AsyncBelongsTo<Model>
, where Model
is the type of the model you are creating a relationship to.
If the value is false
, the type is Model
, where Model
is the type of the model you are creating a relationship to.
So, for example, you might define a class like this:
These are type-safe to define as always present, that is to leave off the ?
optional marker:
accessing an async relationship will always return an AsyncBelongsTo<Model>
object, which itself may or may not ultimately resolve to a value—depending on the API response—but will always be present itself.
accessing a non-async relationship which is known to be associated but has not been loaded will trigger an error, so all access to the property will be safe if it resolves at all.
Note, however, that this type-safety is not a guarantee of there being no runtime error: you still need to uphold the contract for non-async relationships (that is: loading the data first, or side-loading it with the request) to avoid throwing an error!
@hasMany
The type returned by the @hasMany
decorator depends on whether the relationship is { async: true }
(which it is by default).
If the value is true
, the type you should use is AsyncHasMany<Model>
, where Model
is the type of the model you are creating a relationship to.
If the value is false
, the type is SyncHasMany<Model>
, where Model
is the type of the model you are creating a relationship to.
So, for example, you might define a class like this:
The same basic rules about the safety of these lookups as with @belongsTo
apply to these types. The difference is just that in @hasMany
the resulting types are arrays rather than single objects.
When working with TypeScript in Ember tests, there are a few differences in your experience, and there are also differences in how you should handle testing app code vs. addon code.
One major difference when working with TypeScript in app code is that once your app is fully converted, there are a bunch of kinds of tests you just don't need to write any more: things like testing bad inputs to functions. We'll use an admittedly silly and contrived example here, an add
function to add two numbers together, so that we can focus on the differences between JavaScript and TypeScript, rather than getting hung up on the details of this particular function.
First, the function we're testing might look like this.
Then the test for it might look something like this:
In TypeScript, that wouldn't make any sense at all, because we'd simply add the types to the function declaration:
We might still write tests to make sure what we actually got back was what we expected—
—but there are a bunch of things we don't need to test. All of those special bits of handling for the case where we pass in a string
or undefined
or whatever else? We can drop that. Notice, too, that we can drop the assertion from our function definition, because the compiler will check this for us:
Note, however, that this only applies to app code. If you're writing an Ember addon (or any other library), you cannot assume that everyone consuming your code is using TypeScript. You still need to account for these kinds of cases. This will require you to do something that probably feels a bit gross: casting a bunch of values as any
for your tests, so that you can test what happens when people feed bad data to your addon!
Let's return to our silly example with an add
function. Our setup will look a lot like it did in the JavaScript-only example—but with some extra type coercions along the way so that we can invoke it the way JavaScript-only users might.
First, notice that in this case we’ve added back in our assert
in the body of the function. The inputs to our function here will get checked for us by any TypeScript users, but this way we are still doing the work of helping out our JavaScript users.
Now, back in our test file, we’re similarly back to testing all those extra scenarios, but here TypeScript would actually stop us from even having these tests work at all if we didn’t use the as
operator to throw away what TypeScript knows about our code!
TestContext
A common scenario in Ember tests, especially integration tests, is setting some value on the this
context of the tests, so that it can be used in the context of the test. For example, we might need to set up a User
type to pass into a Profile
component.
We’re going to start by defining a basic User
and Profile
so that we have a good idea of what we’re testing.
The User
type is very simple, just an interface
:
Then our component might be defined like this:
Now, with that setup out of the way, let’s get back to talking about the text context! We need to set up a User
to pass into the test. With TypeScript on our side, we can even make sure that it actually matches up to the type we want to use!
This is a decent test, and TypeScript actually makes the experience of writing certain parts of it pretty nice. Unfortunately, though, it won’t type-check. TypeScript reports that the user
field doesn't exist on the TestContext
. Now, TypeScript does know that QUnit sets up that helpfully-named TestContext
—so a lot of the things we can do in tests work out of the box—but we haven’t told TypeScript that this
now has a user
property on it.
To inform TypeScript about this, we need to tell it that the type of this
in each test assertion includes the user
property, of type User
. We’ll start by importing the TestContext
defined by Ember’s test helpers, and extending it:
Putting it all together, this is what our updated test definition would look like:
Now everything type-checks again, and we get the nice auto-completion we’re used to when dealing with this.user
in the test body.
If you’ve been around TypeScript a little, and you look up the type of the TestContext
and realize its an interface, you might be tempted to reach for declaration merging here. Don’t! If you do that, every single test in your entire application will now have a user: User
property on it!
There are still a couple things to be careful about here, however. First, we didn’t specify that the this.user
property was optional. That means that TypeScript won’t complain if you do this.user
before assigning to it. Second, every test in our module gets the same Context
. Depending on what you’re doing, that may be fine, but you may end up needing to define multiple distinct test context extensions. If you do end up needing to define a bunch of different test context extension, that may be a sign that this particular set of tests is doing too much. That in turn is probably a sign that this particular component is doing too much!
This can be fixed by importing qunit-dom
in your test module:
Most existing applications make heavy use of the pre-Octane (“legacy”) Ember programming model, and we support that model—with caveats.
Several parts of Ember Octane (specifically: routes, controllers, services, and class-based helpers) continue to use these concepts under the hood, and our types support that—so understanding them may be important at times.
The rest of this guide is dedicated to helping you understand how ember-cli-typescript
and the classic Ember system interact.
When working with the legacy Ember object model, EmberObject
, there are a number of caveats and limitations you need to be aware of. For today, these caveats and limitations apply to any classes which extend directly from EmberObject
, or which extend classes which themselves extend EmberObject
:
Component
– meaning classic Ember components, which imported from @ember/component
, not Glimmer components which are imported from @glimmer/component
and do not extend the EmberObject
base class.
Controller
Helper
– note that this applies only to the class form. Function-based helpers do not involve the EmberObject
base class.
Route
Router
Service
Ember Data’s Model
class
Additionally, Ember’s mixin system is deeply linked to the semantics and implementation details of EmberObject
, and it has the most caveats and limitations.
In the future, some of these may be able to drop their EmberObject
base class dependency, but that will not happen till at least the next major version of Ember, and these guides will be updated when that happens.
While we describe here how to use types with classic (mixin-based) classes insofar as they do work, there are many failure modes. As a result, we strongly recommend moving away from both classic classes and mixins, and as quickly as possible. This is the direction the Ember ecosystem as a whole is moving, but it is especially important for TypeScript users.
You often need to define this
in actions hashes, computed properties, etc. That in turn often leads to problems with self-referential this
: TypeScript simply cannot figure out how to stop recursing through the definitions of the type.
Additionally, even when you get past the endlessly-recursive type definition problems, when enough mixins are resolved TypeScript will occasionally just give up because it cannot resolve the property or method you're interested in across the many shared base classes.
Finally, when you have "zebra-striping" of your classes between classic classes and native classes, your types will often stop resolving.
EmberObject
EmberObject
-descended classesWe 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 some type utilities to let us get the resolved value returned by a route’s model hook:
How that works:
Resolved<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) uses TS's mapped types to say "the property named model
on R
Putting those all together, ModelFrom<Route>
ends up giving you the resolved value returned from the model
hook for a given route:
model
on the controllerWe 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:
Now, our controller’s model
property will always stay in sync with the corresponding route’s model hook.
Note: this 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. If you do change the type there, you'll need to define the type in some other way and make sure your route's model is defined another way.
Mixins are fundamentally hostile to robust typing with TypeScript. While you can supply types for them, you will regularly run into problems with self-referentiality in defining properties within the mixins.
As a stopgap, you can refer to the type of a mixin using the typeof
operator.
In general, however, prefer to use one of the following four strategies for migrating away from mixins before attempting to convert code which relies on them to TypeScript:
If the mixin is a way of supplying shared behavior (not data), extract it to utility functions, usually just living in module scope and imported and exported as needed.
If the mixin is a way of supplying non-shared state which follows the lifecycle of a given object, replace it with a utility class instantiated in the owning class's constructor
(or init
for legacy classes).
If the mixin is a way of supplying long-lived, shared state, replace it with a service and inject it where it was used before. This pattern is uncommon, but sometimes appears when mixing functionality into multiple controllers or services.
You can also use inheritance and class decorators to accomplish some of the same semantics as mixins classically supplied. However, these patterns are more fragile and therefore not recommended.
You will sometimes see Duplicate identifier errors when type-checking your application.
An example duplicate identifier error:
This occurs whenever your yarn.lock
or package-lock.json
files include more than a single copy of a given set of type definitions—here, types for @ember/object
, named @types/ember__object
. See below for details on the package manager behavior, and Understanding the Package Names for details on the package names.
There are currently three recommended workarounds for this:
If using npm
, you can use npm upgrade --depth=1 @types/ember__object
to upgrade just that specific dependency and anywhere it is used as a transitive dependency of your top-level dependencies. You can also use its npm dedupe
command, which may resolve the issue.
If using yarn
, you can specify a specific version of the package to use in the "resolutions"
key in package.json
. For example, if you saw that you had @types/ember__object@3.0.8
from the default package installs but @types/ember__object@3.0.5
from some-cool-ts-addon
, you could force yarn to use 3.0.8
like so:
However, later installs may introduce conflicting versions of the types, simply by way of yarn's normal update rules. TypeScript requires that there be one and only one type definition a given item can resolve to. Yarn actively avoids changing a previously-installed version of a transitive dependency when a newly installed package depends on the same dependency transitively. Thus, if one of your dependencies also depends on the same package from @types/*
that you do, and you upgrade your dependence on that type by editing your package.json
file and running yarn
or npm install
again, TypeScript will suddenly start offering the error described in detail above:
Duplicate identifier 'EmberObject'.ts(2300)
Let's imagine three packages, A
, B
, and C
, where A
is your app or library, and B
and C
have the following versions and dependencies:
C
is currently at version 1.2.3
.
B
is at version 4.5.6
. It depends on C
with a *
dependency. So the dependencies
key in its package.json
looks like this:
Now, you install only B
(this is the equivalent of installing just the basic type definitions in your package):
The first time you install these, you will get a single version of C
– 1.2.3
.
Now, let's say that C
publishes a new version, 1.2.4
, and A
(your app or library) adds a dependency on both C
like so:
When your package manager runs (especially in the case of yarn
), it goes out of its way to leave the existing installation of C
in place, while adding a new version for you as a top-level consumer. So now you have two versions of C
installed in your node_modules
directory: 1.2.3
(for B
) and 1.2.4
(for A
, your app or library).
What's important to understand here is that this is exactly the behavior you want as the default in the Node ecosystem. Automatically updating a transitive dependency—even when the change is simply a bug fix release—can cause your entire app or library to stop working. If one of your dependencies accidentally depended on that buggy behavior, and adding a direct dependency on the fixed version caused the buggy version to be upgraded, you're just out of luck. Yarn accounts for this by resolving packages to the same version during initial installation, but leaving existing package resolutions as they are when adding new dependencies later.
Unfortunately, this is also the opposite of what you want for TypeScript, which needs a single source of truth for the types in your app or library. When you install the type definitions, and then later install a package which transitively depends on those type definitions, you end up with multiple sources of truth for the types.
The solutions listed above both make sure npm apd Yarn only install a single version of the package.
Explicitly upgrading the dependencies or using dedupe
resolves to a single version in npm.
Specifying a version in the "resolutions"
field in your package.json
simply forces Yarn to resolve every reference to that package to a single version. This actually works extremely well for types, but it means that every time you either update the types package(s) yourself or update a package which transitively depends on them, you have to edit this value manually as well.
Uninstalling and reinstalling both the impacted packages and all the packages which transitively depend on them gives you the same behavior as an initial install… because that's exactly what you're doing. The downside, of course, is that you have to identify and uninstall and reinstall all top-level packages which transitively depend on the files, and this introduces risk by way of other transitive dependencies being updated.
There are a number of important changes between ember-cli-typescript v1 and v2, which mean the upgrade process is straightforward but specific:
Update ember-cli-babel. Fix any problems introduced during the upgrade.
Update ember-decorators. Fix any problems introduced during the upgrade.
Update ember-cli-typescript. Follow the detailed upgrade guide below to fix discrepancies between Babel and TypeScript's compiled output.
If you deviate from this order, you are likely to have a much more difficult time upgrading!
ember-cli-typescript requires ember-cli-babel at version 7.1.0 or above, which requires ember-cli 2.13 or above. It also requires @babel/core 7.2.0 or higher.
The recommended approach here is to deduplicate existing installations of the dependency, remove and reinstall ember-cli-babel to make sure that all its transitive dependencies are updated to the latest possible, and then to deduplicate again.
If using yarn:
If using npm:
Note: If you are also using ember-decorators—and specifically the babel-transform that gets added with it—you will need update @ember-decorators/babel-transforms as well (anything over 3.1.0 should work):
If you're on a version of Ember before 3.10, follow the same process of deduplication, reinstallation, and re-deduplication as described for ember-cli-babel above for ember-decorators. This will get you the latest version of ember-decorators and, importantly, its @ember-decorators/babel-transforms dependency.
Now you can simply ember install
the dependency like normal:
Note: To work properly, starting from v2, ember-cli-typescript must be declared as a dependency
, not a devDependency
for addons. With ember install
this migration would be automatically handled for you.
If you choose to make the upgrade manually with yarn or npm, here are the steps you need to follow:
Remove ember-cli-typescript from your devDependencies
.
With yarn:
With npm:
Install the latest of ember-cli-typescript as a dependency
:
With yarn:
With npm:
Run ember generate
:
Since we now integrate in a more traditional way into Ember CLI's build pipeline, there are two changes required for addons using TypeScript.
Addons can no longer use .ts
in app
, because an addon's app
directory gets merged with and uses the host's (i.e. the other addon or app's) preprocessors, and we cannot guarantee the host has TS support. Note that .ts
will continue to work for in-repo addons because the app build works with the host's (i.e. the app's, not the addon's) preprocessors.
Similarly, apps must use .js
to override addon defaults in app
, since the different file extension means apps no longer consistently "win" over addon versions (a limitation of how Babel + app merging interact).
ember-cli-typescript v2 uses Babel to compile your code, and the TypeScript compiler only to check your code. This makes for much faster builds, and eliminates the differences between Babel and TypeScript in the build output that could cause problems in v1. However, because of those differences, you’ll need to make a few changes in the process of upgrading.
Any place where a type annotation overrides a getter
Fields like element
, disabled
, etc. as annotated defined on a subclass of Component
and (correctly) not initialized to anything, e.g.:
This breaks because element
is a getter on Component
. This declaration then shadows the getter declaration on the base class and stomps it to undefined
(effectively Object.defineProperty(this, 'element', void 0)
. (It would be nice to use declare
here, but that doesn't work: you cannot use declare
with a getter in a concrete subclass.)
Two solutions:
Annotate locally (slightly more annoying, but less likely to troll you):
Use a local getter:
Notably, this is not a problem for Glimmer components, so migrating to Octane will also help!
const enum
is not supported at all. You will need to replace all uses of const enum
with simply enum
or constants.
Using ES5 getters or setters with this
type annotations is not supported through at least Babel 7.3. However, they should also be unnecessary with ES6 classes, so you can simply remove the this
type annotation.
Trailing commas after rest function parameters (function foo(...bar[],) {}
) are disallowed by the ECMAScript spec, so Babel also disallows them.
Re-exports of types have to be disambiguated to be types, rather than values. Neither of these will work:
In both cases, Babel attempts to emit a value export, not just a type export, and fails because there is no actual value to emit. You can do this instead as a workaround:
import
s don't resolveYou'll frequently see errors for imports which TypeScript doesn't know how to resolve. These won't stop the build from working; they just mean TypeScript doesn't know where to find those.
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.
Addons need to import templates from the associated .hbs
file to bind to the layout of any components they export. The TypeScript compiler will report that it cannot resolve the module, since it does not know how to resolve files ending in .hbs
. To resolve this, you can provide this set of definitions to my-addon/types/global.d.ts
, which will allow the import to succeed:
TypeScript won't detect a mismatch between this action and the corresponding call in the template:
Likewise, it won't notice a problem when you use the send
method:
Testing with TypeScript mostly works just the same as you'd expect in a non-TypeScript Ember application—so if you're just starting out with Ember, we recommend you read the official Ember first. The rest of this guide assumes you're already comfortable with testing in Ember!
Here we’re using the assert
from @ember/debug
. If you’re not familiar with it, you might want to take a look at its ! It’s a development-and-test-only helper that gets stripped from production builds, and is very helpful for this kind of thing!
Not familiar with how we define a Glimmer Component
and its arguments? Check out !
Then, in every test
callback, we need to :
When writing , you will use lots of assert.dom()
calls.
Out of the box, Typescript will complain that Property 'dom' does not exist on type 'Assert'.
.
We emphasize the happy path of working with Ember in the . However, there are times you’ll need to understand these details:
The Ember mixin system is the legacy Ember construct TypeScript supports least well, as described in . While this may not be intuitively obvious, the classic class syntax simply is the mixin system. Every classic class creation is a case of mixing together multiple objects to create a new base class with a shared prototype. The result is that any time you see the classic .extend({ ... })
syntax, regardless of whether there is a named mixin involved, you are dealing with Ember's legacy mixin system. This in turn means that you are dealing with the parts of Ember which TypeScript is least able to handle well.
The includes guides for migrating , along with for dealing with specific kinds of mixins in your codebase.
In general, we recommend (following the Ember Octane guides) that any class which extends directly from the EmberObject
base class eliminate any use of EmberObject
-specific API and convert to standalone class, with no base class at all. You can follow the workflow to eliminate the base class—switching from init
to constructor
, getting rid of uses of methods like this.set
and this.get
in favor of using standalone set
and get
, and so on.
The framework base classes which depend on EmberObject
cannot follow the exact same path. However, as long as you are using native class syntax, all of these (Component
, Controller
, Helper
, etc.) work nicely and safely with TypeScript. In each of these cases, the same caveats apply as with EmberObject
itself, and you should follow the workflow with them as well if you are converting an existing app or addon. However, because these base classes themselves descend from EmberObject
, you will not be able to remove the base classes as you can with your own classes which descend directly from EmberObject
. Instead, you will continue to extend from the Ember base classes:
For functionality which encapsulates DOM modification, rewrite as a custom modifier using .
You can identify the dependencies which installed the type dependencies transitively, and uninstall and reinstall them. For example, if running yarn why
reported you had one version of @types/ember__object
from , and one from some-cool-ts-addon
, you could run this:
You may also be able to use , but this does not work 100% of the time, so if you try it and are still seeing the issues, try one of the solutions above.
When you are using TypeScript in your Ember application, you consume Ember's types through , the tool the TypeScript team built to power the @types/*
definitions. That tooling examines the dependencies implied by the package imports and generates a package.json
with those types specified with a *
dependency version. On initial installation of your dependencies, yarn installs the highest version of the package available, and correctly deduplicates that across both your own package and all the @types
packages which reference each other.
Have an idea for an item that should fit here? We'd love to help you help us make this experience more awesome for everyone.
While TS already works nicely for many things in Ember, there are a number of corners where it won't help you out. Some of them are just a matter of further work on updating the ; others are a matter of further support landing in TypeScript itself, or changes to Ember's object model.
Writing these missing type definitions is a great way to pitch in! Jump in #topic-typescript
on the and we'll be happy to help you.
There are two variants of Ember’s computed properties you may encounter:
the decorator form used with native (ES6) classes
the callback form used with classic classes (based on EmberObject)
Note that it is impossible for @computed
to know whether the keys you pass to it are allowed or not. Migrating to Octane eliminates this issue, since you mark reactive root state with @tracked
and leave getters undecorated, rather than vice versa.
Computed properties in the classic object model take a callback instead:
This definition will not type-check, however. You will need to explicitly write out a this
type for computed property callbacks for get
and set
to type-check correctly:
Note that this does not always work: you may get warnings from TypeScript about the item being defined in terms of itself.
Accordingly, we strongly recommend migrating classic classes to ES native classes before adding TypeScript!