What about missing types?
Gradually typing your app
See "Gradual Typing Hacks" for strategies for incrementally adding types to your app.
Install types for libraries
You'll want to use library type definitions as much as possible. Many packages ship their own type definitions, and many others have community-maintained definitions from DefinitelyTyped, available in the @types
name space. The first thing you should do is to look for types from other libraries: it will mean using fewer "Gradual Typing Hacks" and getting a lot more help both from your editor and from the compiler.
The types
directory
During installation, we create a types
directory in the root of your application and add a "paths"
mapping to your tsconfig.json
that includes that directory in any type lookups TypeScript tries to do. This is convenient for a few things:
- global types for your project (see the next section)
- writing types for libraries that do not have any types
These are all fallbacks, of course, you should use the types supplied directly with a package when possible.
Global types for your project
At the root of your application or addon, we include a types/<your project>
directory with an index.d.ts
file in it. Anything which is part of your project 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.
We automatically configure index.d.ts
to be ready for Glint, which will make type checking work with Ember's templates. The default configuration only supports Ember's classic pairing of separate .ts
and .hbs
files, but Glint also supports the <template>
format with .gts
files. See the corresponding package README for more details. (Once Ember enables <template>
by default, so will our Glint configuration!)
Environment configuration typings
Along with the @types/
files mentioned above, we add 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 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.
Type Narrowing with Ember Debug Assert
Ember's assert
function from @ember/debug
is super useful for "type narrowing"—TypeScript's process of refining types to more specific types than originally declared. If you're not familiar with assert
, you might want to take a look at its API docs! It's a development-and-test-only helper that gets stripped from production builds, and is very helpful for this kind of thing!
For example, let's pretend we're writing an addon that provides a totalLength
helper to tally up the total length of an array of strings passed to it. Because addon authors cannot guarantee that their users will be using TypeScript, we've typed the positional arguments as an array of unknown
so that TypeScript will ensure we've handled every possible valid or invalid argument a user might pass.
We can use assert
to ensure that if a user passes an array containing non-string values, our addon will error in tests and development.
import { assert } from '@ember/debug';
function totalLength(positional: unknown[]) {
assert(
'all positional args to `total-length` must be strings',
positional.every((arg): arg is string => typeof arg === 'string')
);
// TypeScript now knows that `positional` is a `string[]` because we asserted above
return positional.reduce((sum, s) => sum + s.length, 0);
}
And, the types for assert
ensure that TypeScript can use the condition you pass to properly narrow the types, because assert
is typed as an assertion function.
export interface AssertFunc {
(desc: string, condition: unknown): asserts condition;
(desc: string): never;
}
Strictness
You can enable TypeScript's current strictest configuration by including the @tsconfig/strictest
base before the @tsconfig/ember
base in your tsconfig.json
:
{
extends: [
'@tsconfig/strictest/tsconfig.json',
'@tsconfig/ember/tsconfig.json',
],
// ...
}