From a959a477ad8041f69e38aa59c81965dcbb99ccc2 Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Sun, 27 Aug 2023 16:31:10 -0700 Subject: [PATCH 01/63] Add TypeScript guides under construction page --- guides/release/pages.yml | 5 +++++ guides/release/using-typescript/index.md | 1 + 2 files changed, 6 insertions(+) create mode 100644 guides/release/using-typescript/index.md diff --git a/guides/release/pages.yml b/guides/release/pages.yml index e60257a739..95e9bdf02c 100644 --- a/guides/release/pages.yml +++ b/guides/release/pages.yml @@ -249,6 +249,11 @@ pages: - title: "Managing Dependencies" url: "index" +- title: "Using TypeScript" + url: "using-typescript" + pages: + - title: "Getting Started With TypeScript" + url: "index" - title: "Developer Tools" url: "toc-heading_developer-tools" is_heading: true diff --git a/guides/release/using-typescript/index.md b/guides/release/using-typescript/index.md new file mode 100644 index 0000000000..a7f542457e --- /dev/null +++ b/guides/release/using-typescript/index.md @@ -0,0 +1 @@ +Under construction! From 9ca1517f4796d2bf4581de8c444eb0e36ad78cb2 Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Thu, 31 Aug 2023 09:50:20 -0700 Subject: [PATCH 02/63] WIP: Copy-pasta guides from ember-cli-typescript --- guides/release/pages.yml | 8 +- .../release/using-typescript/configuration.md | 43 +++ .../using-typescript/getting-started.md | 41 +++ guides/release/using-typescript/index.md | 42 ++- .../release/using-typescript/ts-and-ember.md | 317 ++++++++++++++++++ 5 files changed, 449 insertions(+), 2 deletions(-) create mode 100644 guides/release/using-typescript/configuration.md create mode 100644 guides/release/using-typescript/getting-started.md create mode 100644 guides/release/using-typescript/ts-and-ember.md diff --git a/guides/release/pages.yml b/guides/release/pages.yml index 95e9bdf02c..1d5258f304 100644 --- a/guides/release/pages.yml +++ b/guides/release/pages.yml @@ -252,8 +252,14 @@ - title: "Using TypeScript" url: "using-typescript" pages: - - title: "Getting Started With TypeScript" + - title: "TypeScript: Introduction" url: "index" + - title: "Getting Started with TypeScript" + url: "getting-started" + - title: "TypeScript Configuration" + url: "configuration" + - title: "TypeScript and Ember" + url: "ts-and-ember" - title: "Developer Tools" url: "toc-heading_developer-tools" is_heading: true diff --git a/guides/release/using-typescript/configuration.md b/guides/release/using-typescript/configuration.md new file mode 100644 index 0000000000..bfc3bc72d8 --- /dev/null +++ b/guides/release/using-typescript/configuration.md @@ -0,0 +1,43 @@ + + +## `tsconfig.json` + +We generate a good default [`tsconfig.json`](https://github.com/typed-ember/ember-cli-typescript/blob/master/blueprint-files/ember-cli-typescript/tsconfig.json), which will usually make everything _Just Work™_. In general, you may customize your TypeScript build process as usual using the `tsconfig.json` file. + +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!): + +1. 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. + + 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/[Broccoli](https://broccoli.build) pipeline will continue to use its own temp folder. + +2. 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. +3. 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. +4. 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 [the TypeScript reference materials for `tsconfig.json`](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html). + +## Enabling Sourcemaps + +To enable TypeScript sourcemaps, you'll need to add the corresponding configuration for Babel to your `ember-cli-build.js` file: + +```typescript +const app = new EmberApp(defaults, { + babel: { + sourceMaps: 'inline', + }, +}); +``` + +(Note that this _will_ noticeably slow down your app rebuilds.) + +If you are using [Embroider](https://github.com/embroider-build/embroider), you might need to include [devtool](https://webpack.js.org/configuration/devtool/) in your webpack configuration: + +```ts +return require('@embroider/compat').compatBuild(app, Webpack, { + packagerOptions: { + webpackConfig: { + devtool: 'source-map' + } + } +} +``` + +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 [the blueprint](https://github.com/typed-ember/ember-cli-typescript/blob/master/blueprint-files/ember-cli-typescript/files/tsconfig.json). diff --git a/guides/release/using-typescript/getting-started.md b/guides/release/using-typescript/getting-started.md new file mode 100644 index 0000000000..b459f7103c --- /dev/null +++ b/guides/release/using-typescript/getting-started.md @@ -0,0 +1,41 @@ +To start a new Ember project with TypeScript, you can add the `--typescript` flag when you run [`ember new`](../../getting-started/quick-start): + +```bash +ember new my-typescript-app --typescript +``` + +All dependencies will be added to your `package.json`, and you're ready to roll! + +Using the `--typescript` flag changes the output of `ember new` in a few ways: + +- project files will be generated with `.ts` extensions instead of `.js` +- packages will be installed and files will be generated to make TypeScript work in your project +- TypeScript-specific config is set + +## Packages + +When `ember new` is run with the `--typescript` flag set, we install all of the following packages at their current "latest" value: + +- `typescript` +- `@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/qunit` +- `@types/rsvp` + + + +## Files + +We also add the following files to your project: + +- [`tsconfig.json`](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) + +- `types//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**](https://github.com/typed-ember/ember-cli-typescript/tree/3a434def8b8c8214853cea0762940ccedb2256e8/docs/getting-started/docs/ts/using-ts-effectively/README.md#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](installation.md#environment-and-configuration-typings) for details + +## Config + + diff --git a/guides/release/using-typescript/index.md b/guides/release/using-typescript/index.md index a7f542457e..9fae3c127d 100644 --- a/guides/release/using-typescript/index.md +++ b/guides/release/using-typescript/index.md @@ -1 +1,41 @@ -Under construction! +This guide is designed to help you get up and running with TypeScript in an Ember app. + +
+
+
+
Zoey says...
+
+

+ This is not an introduction to TypeScript or Ember. Throughout this guide, we’ll link back to the TypeScript docs and the Ember Guides 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 [Getting Started with TypeScript](./getting-started) + + + +- If you're totally new to using TypeScript with Ember, start with [TypeScript and Ember](./ts/README.md). + +- Once you have a good handle on the basics, you can dive into the guides to working with the APIs specific to [Ember](./ember/README.md) and [Ember Data](./ember-data/README.md). + +- If you're working with legacy (pre-Octane) Ember and TypeScript together, you should read [the Legacy Guide](./legacy/README.md). + +- Looking for type-checking in Glimmer templates? Check out [Glint](https://typed-ember.gitbook.io/glint/). + +## Why TypeScript? + +What is TypeScript, and why should you adopt it? + +> TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale. +> +> — [typescriptlang.org](http://www.typescriptlang.org) + +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 refactoring, automatic imports, and more. diff --git a/guides/release/using-typescript/ts-and-ember.md b/guides/release/using-typescript/ts-and-ember.md new file mode 100644 index 0000000000..d9715e641f --- /dev/null +++ b/guides/release/using-typescript/ts-and-ember.md @@ -0,0 +1,317 @@ + + +# TypeScript and Ember + +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: + +- [TypeScript docs](https://www.typescriptlang.org/docs/index.html) +- [TypeScript Deep Dive](https://basarat.gitbook.io/typescript/) +- [Ember docs](https://emberjs.com/learn/) + +## Outline + +- [Using TypeScript With Ember Effectively](using-ts-effectively.md) +- [Decorators](decorators.md) +- [Current limitations](current-limitations.md) +- [Building Addons in TypeScript](with-addons.md) +- [Understanding the `@types` Package Names](package-names.md) + + + +# Using TypeScript With Ember Effectively + +## Incremental adoption + +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): + +```json +{ + "compilerOptions": { + // Strictness settings -- you should *not* change these: Ember code is not + // guaranteed to type check with these set to looser values. + "strict": true, + "noUncheckedIndexedAccess": true, + + // You should feel free to change these, especially if you are already + // covering them via linting (e.g. with @typescript-eslint). + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noPropertyAccessFromIndexSignature": true + } +} +``` + +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. + +![example of a build error during live reload](https://user-images.githubusercontent.com/108688/38774630-7d9224d4-403b-11e8-8dbc-87dad977a4c4.gif) + +## What about missing 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. + +## Install other types + +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](https://github.com/DefinitelyTyped/DefinitelyTyped), or on npm under the `@types` namespace. (In the future we hope to maintain a list of known types; keep your eyes open!) + +## The `types` directory + +During 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 + +### Global types for your package + +At the root of your application or addon, we include a `types/` 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](https://guides.emberjs.com/v2.18.0/configuring-ember/disabling-prototype-extensions/), you can remove these declarations entirely; we include them because they're enabled in most Ember applications today. + +### Environment configuration typings + +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 `/config/environment` into your build for consumption at runtime. + +## String-keyed lookups + +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: + +### Nested keys in `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: + +1. **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! + + ```typescript + import { get } from '@ember/object'; + + // -- Type-safe but ugly --// + get(get(get(someObject, 'deeply'), 'nested'), 'key'); + ``` + +2. **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. + + ```typescript + // @ts-ignore + get(someObject, 'deeply.nested.key'); + ``` + + It's usually best to include an explanation of _why_ you're ignoring a lookup! + +### Service and controller injections + +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_. + +```typescript +// my-app/services/my-session.ts +import Service from '@ember/service'; +import RSVP from 'rsvp'; + +export default class MySession extends Service { + login(email: string, password: string): RSVP.Promise { + // login and return the confirmation message + } +} + +declare module '@ember/service' { + interface Registry { + 'my-session': MySession; + } +} +``` + +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: + +```typescript +// my-app/components/user-profile.ts +import Component from '@ember/component'; +import { inject as service } from '@ember/service'; + +import MySession from 'my-app/services/my-session'; + +export default class UserProfile extends Component { + @service declare mySession: MySession; + + login(email: string, password: string) { + this.mySession.login(email, password); + } +} +``` + +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](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#the-usedefineforclassfields-flag-and-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.) + +### Earlier Ember versions + +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`](https://www.emberjs.com/api/ember/release/classes/ComputedProperty), because [native ES5 getters](https://github.com/emberjs/rfcs/blob/master/text/0281-es5-getters.md) 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: + +```typescript +import Component from '@ember/component'; + +export default class UserProfile extends Component { + username?: string; +} +``` + +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](https://github.com/pzuraq/ember-native-class-polyfill) + +### Ember Data lookups + +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 + + ```typescript + import Model from '@ember-data/model'; + + export default class UserMeta extends Model {} + + declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + 'user-meta': UserMeta; + } + } + ``` + +- Adapters + + ```typescript + import Adapter from '@ember-data/adapter'; + + export default class UserMeta extends Adapter {} + + declare module 'ember-data/types/registries/adapter' { + export default interface AdapterRegistry { + 'user-meta': UserMeta; + } + } + ``` + +- Serializers + + ```typescript + import Serializer from '@ember-data/serializer'; + + export default class UserMeta extends Serializer {} + + declare module 'ember-data/types/registries/serializer' { + export default interface SerializerRegistry { + 'user-meta': UserMeta; + } + } + ``` + +- Transforms + + ```typescript + import Transform from '@ember-data/serializer/transform'; + + export default class ColorTransform extends Transform {} + + declare module 'ember-data/types/registries/transform' { + export default interface TransformRegistry { + color: ColorTransform; + } + } + ``` + +#### Opt-in unsafety + +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: + +```typescript +import Model from '@ember-data/model'; +import Adapter from '@ember-data/adapter'; +import Serializer from '@ember-data/serializer'; + +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + [key: string]: Model; + } +} +declare module 'ember-data/types/registries/adapter' { + export default interface AdapterRegistry { + [key: string]: Adapter; + } +} +declare module 'ember-data/types/registries/serializer' { + export default interface SerializerRegistry { + [key: string]: Serializer; + } +} +``` + +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! + +#### Fixing the Ember Data `error TS2344` problem + +If 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: + +```text +node_modules/@types/ember-data/index.d.ts(920,56): error TS2344: Type 'any' does not satisfy the constraint 'never'. +``` + +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: + +```typescript +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + [key: string]: unknown; + } +} +``` + +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! + +### Class property setup errors + +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](using-ts-effectively.md#service-and-controller-injections). 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`. + +## Type definitions outside `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](http://www.typescriptlang.org/docs/handbook/compiler-options.html#compiler-options) for details. From acacabdaa0bfd53a39e9eb0a2e819e8dff9ead70 Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Fri, 1 Sep 2023 11:34:46 -0700 Subject: [PATCH 03/63] Apply suggestions from code review Co-authored-by: Chris Krycho --- .../release/using-typescript/configuration.md | 4 +- .../using-typescript/getting-started.md | 17 ++++- guides/release/using-typescript/index.md | 3 + .../release/using-typescript/ts-and-ember.md | 73 ++++++------------- 4 files changed, 41 insertions(+), 56 deletions(-) diff --git a/guides/release/using-typescript/configuration.md b/guides/release/using-typescript/configuration.md index bfc3bc72d8..cdb3b617c3 100644 --- a/guides/release/using-typescript/configuration.md +++ b/guides/release/using-typescript/configuration.md @@ -1,4 +1,4 @@ - +**Note:** 🚧 This section is under construction! 🏗️ The content here may not be fully up to date! ## `tsconfig.json` @@ -40,4 +40,4 @@ return require('@embroider/compat').compatBuild(app, Webpack, { } ``` -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 [the blueprint](https://github.com/typed-ember/ember-cli-typescript/blob/master/blueprint-files/ember-cli-typescript/files/tsconfig.json). +If you're migrating from `ember-cli-typescript`, particularly an older version, to Ember's out-of-the-box TypeScript support, you may also need to update your `tsconfig.json`. Current versions of `ember-cli-typescript` generate the correct config at installation. You do *not* need to install `ember-cli-typescript` for new apps or adding. diff --git a/guides/release/using-typescript/getting-started.md b/guides/release/using-typescript/getting-started.md index b459f7103c..93ae798d8c 100644 --- a/guides/release/using-typescript/getting-started.md +++ b/guides/release/using-typescript/getting-started.md @@ -1,3 +1,5 @@ +**Note:** 🚧 This section is under construction! 🏗️ The content here may not be fully up to date! + To start a new Ember project with TypeScript, you can add the `--typescript` flag when you run [`ember new`](../../getting-started/quick-start): ```bash @@ -10,7 +12,8 @@ Using the `--typescript` flag changes the output of `ember new` in a few ways: - project files will be generated with `.ts` extensions instead of `.js` - packages will be installed and files will be generated to make TypeScript work in your project -- TypeScript-specific config is set +- We configure Babel to transpile TypeScript files. +- We configure TypeScript to type check your project. ## Packages @@ -24,7 +27,17 @@ When `ember new` is run with the `--typescript` flag set, we install all of the - `@types/qunit` - `@types/rsvp` - +
+
+
+
Zoey says...
+
+ Ember also includes its own types compiled directly from its source code, as described in this blog post. We still use the @types packages by default for the sake of compatibility with Ember Data, because Ember Data is not yet compatible with Ember’s native official types. However, if you only do not use Ember Data, we recommend following the instructions in that blog post to switch to the native types, which are guaranteed to always be 100% correct and 100% up to date! +
+
+ +
+
## Files diff --git a/guides/release/using-typescript/index.md b/guides/release/using-typescript/index.md index 9fae3c127d..4da219a491 100644 --- a/guides/release/using-typescript/index.md +++ b/guides/release/using-typescript/index.md @@ -1,5 +1,8 @@ +**Note:** 🚧 This section is under construction! 🏗️ The content here may not be fully up to date! + This guide is designed to help you get up and running with TypeScript in an Ember app. +
diff --git a/guides/release/using-typescript/ts-and-ember.md b/guides/release/using-typescript/ts-and-ember.md index d9715e641f..e1b672b832 100644 --- a/guides/release/using-typescript/ts-and-ember.md +++ b/guides/release/using-typescript/ts-and-ember.md @@ -1,4 +1,4 @@ - +**Note:** 🚧 This section is under construction! 🏗️ The content here may not be fully up to date! # TypeScript and Ember @@ -67,9 +67,7 @@ There is an inherent tradeoff between these two approaches; which works best wil ## Install other types -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](https://github.com/DefinitelyTyped/DefinitelyTyped), or on npm under the `@types` namespace. (In the future we hope to maintain a list of known types; keep your eyes open!) +You'll want to use other type definitions as much as possible. Many packages ship their own type definitions, and many others have community-maintained definitions from [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped), available in the `@types` name space. The first thing you should do is to 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. ## The `types` directory @@ -99,35 +97,17 @@ Ember makes heavy use of string-based APIs to allow for a high degree of dynamic A few of the most common speed-bumps are listed here to help make this easier: -### Nested keys in `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: - -1. **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! - - ```typescript - import { get } from '@ember/object'; - - // -- Type-safe but ugly --// - get(get(get(someObject, 'deeply'), 'nested'), 'key'); - ``` +### Classic `get` or `set` methods -2. **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. +In general, the `this.get` and `this.set` methods on `EmberObject` subclasses and the standalone `get` and `set` functions will work as you'd expect _if_ you're doing lookups only a single layer deep. We do not provide support for deep key lookups like `get(someObj, 'a.b.c')`, because normal property access can works correctly across the whole Ember ecosystem since at least Ember and Ember Data 3.28. - ```typescript - // @ts-ignore - get(someObject, 'deeply.nested.key'); - ``` - - It's usually best to include an explanation of _why_ you're ignoring a lookup! +Since regular property access “just works”, and has for a very long time, you should migrate to using normal property access instead. TypeScript will help make this a smooth process by identifying where you need to handle null and undefined intermediate properties. ### Service and controller injections -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. +Ember looks up services with the `@service` decorator at runtime, using the name of the service 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 to inject isn't available at compile time in the same way. (These same considerations apply to controller injections using the `@inject` decorator from `@ember/controller`.) -The officially supported method for injections with TypeScript uses _decorators_. +Since decorators do not currently have access to enough information to produce an appropriate type by themselves, we need to import and name the type explicitly. For example, we might have `MySession` service which defines a `login` method, defined as usual: ```typescript // my-app/services/my-session.ts @@ -154,7 +134,7 @@ Then we can use the service as we usually would with a decorator, but adding a t import Component from '@ember/component'; import { inject as service } from '@ember/service'; -import MySession from 'my-app/services/my-session'; +import type MySession from 'my-app/services/my-session'; export default class UserProfile extends Component { @service declare mySession: MySession; @@ -171,29 +151,28 @@ Also notice [the `declare` property modifier](https://www.typescriptlang.org/doc (This also holds true for all other service injections, computed property macros, and Ember Data model attributes and relationships.) -### Earlier Ember versions - -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`](https://www.emberjs.com/api/ember/release/classes/ComputedProperty), because [native ES5 getters](https://github.com/emberjs/rfcs/blob/master/text/0281-es5-getters.md) 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: +Finally, you may have noticed the `declare module` at the bottom of the example `MySession` definition: ```typescript -import Component from '@ember/component'; - -export default class UserProfile extends Component { - username?: string; +declare module '@ember/service' { + interface Registry { + 'my-session': MySession; + } } ``` -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. +This definition allows for type-safe lookups with other Ember dependency injection APIs. For example, [the `Owner.lookup` method](https://api.emberjs.com/ember/5.2/classes/Owner#2-method) uses this "registration"—a mapping from the string `'my-session'` to the service type, `MySession`—to provide the correct type: -For users who remain on Ember versions below `3.6`, please use [https://github.com/pzuraq/ember-native-class-polyfill](https://github.com/pzuraq/ember-native-class-polyfill) +```typescript +function dynamicLookup(owner: Owner) { + let mySession = owner.lookup('service:my-session'); + mySession.login("tom@example.com", "password123"); +} +``` ### Ember Data lookups -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. +We use the same basic approach for Ember Data type lookups with string keys as we do for service injections, but here we take advantage of the string "type registration" for the runtime code as well. 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. That is, writing `this.store.findRecord('user', 1)` will give you back a `Promise`. The declarations and changes you need to add to your existing files are: @@ -305,13 +284,3 @@ This works because (a) we include things in your types directory automatically a 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! -### Class property setup errors - -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](using-ts-effectively.md#service-and-controller-injections). 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`. - -## Type definitions outside `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](http://www.typescriptlang.org/docs/handbook/compiler-options.html#compiler-options) for details. From a02e4e596a2bde47d156e0fc7ddf35b142ae3060 Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Sat, 2 Sep 2023 12:47:28 -0700 Subject: [PATCH 04/63] Finish initial copy-pasta from ember-cli-typescript docs --- guides/release/pages.yml | 576 +++++----- .../building-addons-in-typescript.md | 79 ++ guides/release/using-typescript/index.md | 2 +- .../release/using-typescript/ts-and-ember.md | 99 +- .../ts-patterns-for-ember-data.md | 187 +++ .../using-typescript/ts-patterns-for-ember.md | 1017 +++++++++++++++++ 6 files changed, 1671 insertions(+), 289 deletions(-) create mode 100644 guides/release/using-typescript/building-addons-in-typescript.md create mode 100644 guides/release/using-typescript/ts-patterns-for-ember-data.md create mode 100644 guides/release/using-typescript/ts-patterns-for-ember.md diff --git a/guides/release/pages.yml b/guides/release/pages.yml index 1d5258f304..628b808dc7 100644 --- a/guides/release/pages.yml +++ b/guides/release/pages.yml @@ -1,338 +1,344 @@ -- title: "Introduction" - url: "toc-heading_introduction" +- title: 'Introduction' + url: 'toc-heading_introduction' is_heading: true -- title: "Guides and Tutorials" - url: "index" +- title: 'Guides and Tutorials' + url: 'index' skip_toc: true pages: - - title: "Ember.js Guides" - url: "" -- title: "Getting Started" - url: "getting-started" + - title: 'Ember.js Guides' + url: '' +- title: 'Getting Started' + url: 'getting-started' pages: - - title: "How To Use The Guides" - url: "index" - - title: "Quick Start" - url: "quick-start" - - title: "Working with HTML, CSS, and JavaScript" - url: "working-with-html-css-and-javascript" - - title: "Anatomy of an Ember App" - url: "anatomy-of-an-ember-app" -- title: "Tutorial" - url: "tutorial" + - title: 'How To Use The Guides' + url: 'index' + - title: 'Quick Start' + url: 'quick-start' + - title: 'Working with HTML, CSS, and JavaScript' + url: 'working-with-html-css-and-javascript' + - title: 'Anatomy of an Ember App' + url: 'anatomy-of-an-ember-app' +- title: 'Tutorial' + url: 'tutorial' pages: - - title: "Part 1" - url: "part-1" + - title: 'Part 1' + url: 'part-1' pages: - - title: "Introduction" - url: "index" - - title: "Orientation" - url: "orientation" - - title: "Building Pages" - url: "building-pages" - - title: "Automated Testing" - url: "automated-testing" - - title: "Component Basics" - url: "component-basics" - - title: "More About Components" - url: "more-about-components" - - title: "Interactive Components" - url: "interactive-components" - - title: "Reusable Components" - url: "reusable-components" - - title: "Working With Data" - url: "working-with-data" - - title: "Recap" - url: "recap" - - title: "Part 2" - url: "part-2" + - title: 'Introduction' + url: 'index' + - title: 'Orientation' + url: 'orientation' + - title: 'Building Pages' + url: 'building-pages' + - title: 'Automated Testing' + url: 'automated-testing' + - title: 'Component Basics' + url: 'component-basics' + - title: 'More About Components' + url: 'more-about-components' + - title: 'Interactive Components' + url: 'interactive-components' + - title: 'Reusable Components' + url: 'reusable-components' + - title: 'Working With Data' + url: 'working-with-data' + - title: 'Recap' + url: 'recap' + - title: 'Part 2' + url: 'part-2' pages: - - title: "Introduction" - url: "index" - - title: "Route Params" - url: "route-params" - - title: "Service Injection" - url: "service-injection" - - title: "EmberData" - url: "ember-data" - - title: "Provider Components" - url: "provider-components" - - title: "Recap" - url: "recap" + - title: 'Introduction' + url: 'index' + - title: 'Route Params' + url: 'route-params' + - title: 'Service Injection' + url: 'service-injection' + - title: 'EmberData' + url: 'ember-data' + - title: 'Provider Components' + url: 'provider-components' + - title: 'Recap' + url: 'recap' -- title: "Core Concepts" - url: "toc-heading_core-concepts" +- title: 'Core Concepts' + url: 'toc-heading_core-concepts' is_heading: true -- title: "Components" +- title: 'Components' url: components pages: - - title: "Templates are HTML" - url: "index" - - title: "Introducing Components" - url: "introducing-components" - - title: "Component Arguments and HTML Attributes" - url: "component-arguments-and-html-attributes" - - title: "Conditional Content" - url: "conditional-content" - - title: "Block Content" - url: "block-content" - - title: "Helper Functions" - url: "helper-functions" - - title: "Component State and Actions" - url: "component-state-and-actions" - - title: "Looping Through Lists" - url: "looping-through-lists" + - title: 'Templates are HTML' + url: 'index' + - title: 'Introducing Components' + url: 'introducing-components' + - title: 'Component Arguments and HTML Attributes' + url: 'component-arguments-and-html-attributes' + - title: 'Conditional Content' + url: 'conditional-content' + - title: 'Block Content' + url: 'block-content' + - title: 'Helper Functions' + url: 'helper-functions' + - title: 'Component State and Actions' + url: 'component-state-and-actions' + - title: 'Looping Through Lists' + url: 'looping-through-lists' - title: Template Lifecycle, DOM, and Modifiers url: template-lifecycle-dom-and-modifiers - - title: "Built-in Components" - url: "built-in-components" -- title: "Routing" - url: "routing" + - title: 'Built-in Components' + url: 'built-in-components' +- title: 'Routing' + url: 'routing' pages: - - title: "Introduction" - url: "index" - - title: "Defining Your Routes" - url: "defining-your-routes" - - title: "Linking Between Routes" - url: "linking-between-routes" + - title: 'Introduction' + url: 'index' + - title: 'Defining Your Routes' + url: 'defining-your-routes' + - title: 'Linking Between Routes' + url: 'linking-between-routes' - title: "Specifying a Route's Model" - url: "specifying-a-routes-model" - - title: "Rendering a Template" - url: "rendering-a-template" - - title: "Redirecting" - url: "redirection" - - title: "Preventing and Retrying Transitions" - url: "preventing-and-retrying-transitions" + url: 'specifying-a-routes-model' + - title: 'Rendering a Template' + url: 'rendering-a-template' + - title: 'Redirecting' + url: 'redirection' + - title: 'Preventing and Retrying Transitions' + url: 'preventing-and-retrying-transitions' isAdvanced: true - - title: "Loading / Error Substates" - url: "loading-and-error-substates" + - title: 'Loading / Error Substates' + url: 'loading-and-error-substates' isAdvanced: true - - title: "Query Parameters" - url: "query-params" - - title: "Asynchronous Routing" - url: "asynchronous-routing" + - title: 'Query Parameters' + url: 'query-params' + - title: 'Asynchronous Routing' + url: 'asynchronous-routing' isAdvanced: true - - title: "Controllers" - url: "controllers" -- title: "Services" - url: "services" + - title: 'Controllers' + url: 'controllers' +- title: 'Services' + url: 'services' pages: - title: Overview - url: "index" -- title: "EmberData" - url: "models" + url: 'index' +- title: 'EmberData' + url: 'models' pages: - - title: "Introduction" - url: "index" - - title: "Defining Models" - url: "defining-models" - - title: "Finding Records" - url: "finding-records" - - title: "Creating, Updating and Deleting" - url: "creating-updating-and-deleting-records" - - title: "Relationships" - url: "relationships" - - title: "Pushing Records into the Store" - url: "pushing-records-into-the-store" + - title: 'Introduction' + url: 'index' + - title: 'Defining Models' + url: 'defining-models' + - title: 'Finding Records' + url: 'finding-records' + - title: 'Creating, Updating and Deleting' + url: 'creating-updating-and-deleting-records' + - title: 'Relationships' + url: 'relationships' + - title: 'Pushing Records into the Store' + url: 'pushing-records-into-the-store' isAdvanced: true - - title: "Handling Metadata" - url: "handling-metadata" + - title: 'Handling Metadata' + url: 'handling-metadata' isAdvanced: true - - title: "Customizing Adapters" - url: "customizing-adapters" + - title: 'Customizing Adapters' + url: 'customizing-adapters' isAdvanced: true - - title: "Customizing Serializers" - url: "customizing-serializers" + - title: 'Customizing Serializers' + url: 'customizing-serializers' isAdvanced: true -- title: "In-Depth Topics" - url: "in-depth-topics" +- title: 'In-Depth Topics' + url: 'in-depth-topics' pages: - - title: "Autotracking In-Depth" - url: "autotracking-in-depth" - - title: "Patterns for Components" - url: "patterns-for-components" - - title: "Patterns for Actions" - url: "patterns-for-actions" - - title: "Making API Requests" - url: "making-api-requests" - - title: "Native Classes In-Depth" - url: "native-classes-in-depth" - - title: "Rendering Values" - url: "rendering-values" + - title: 'Autotracking In-Depth' + url: 'autotracking-in-depth' + - title: 'Patterns for Components' + url: 'patterns-for-components' + - title: 'Patterns for Actions' + url: 'patterns-for-actions' + - title: 'Making API Requests' + url: 'making-api-requests' + - title: 'Native Classes In-Depth' + url: 'native-classes-in-depth' + - title: 'Rendering Values' + url: 'rendering-values' -- title: "Application Development" - url: "toc-heading_application-development" +- title: 'Application Development' + url: 'toc-heading_application-development' is_heading: true -- title: "Application Concerns" - url: "applications" +- title: 'Application Concerns' + url: 'applications' pages: - - title: "Applications and Instances" - url: "index" + - title: 'Applications and Instances' + url: 'index' isAdvanced: true - - title: "Dependency Injection" - url: "dependency-injection" + - title: 'Dependency Injection' + url: 'dependency-injection' isAdvanced: true - - title: "Initializers" - url: "initializers" + - title: 'Initializers' + url: 'initializers' isAdvanced: true - - title: "The Run Loop" - url: "run-loop" + - title: 'The Run Loop' + url: 'run-loop' isAdvanced: true - - title: "Ember Engines" - url: "ember-engines" + - title: 'Ember Engines' + url: 'ember-engines' isAdvanced: true - title: Accessibility url: accessibility pages: - - title: "Intro to Accessibility" - url: "index" - - title: "Application Considerations" - url: "application-considerations" - - title: "Page Template Considerations" - url: "page-template-considerations" - - title: "Component Considerations" - url: "components" - - title: "Testing Considerations" - url: "testing" - - title: "Learning Resources" - url: "learning-resources" -- title: "Configuration" - url: "configuring-ember" + - title: 'Intro to Accessibility' + url: 'index' + - title: 'Application Considerations' + url: 'application-considerations' + - title: 'Page Template Considerations' + url: 'page-template-considerations' + - title: 'Component Considerations' + url: 'components' + - title: 'Testing Considerations' + url: 'testing' + - title: 'Learning Resources' + url: 'learning-resources' +- title: 'Configuration' + url: 'configuring-ember' pages: - - title: "Configuring Your App" - url: "index" - - title: "Configuring Ember CLI" - url: "configuring-ember-cli" - - title: "Handling Deprecations" - url: "handling-deprecations" - - title: "Disabling Prototype Extensions" - url: "disabling-prototype-extensions" - - title: "Specifying the URL Type" - url: "specifying-url-type" - - title: "Embedding Applications" - url: "embedding-applications" - - title: "Feature Flags" - url: "feature-flags" + - title: 'Configuring Your App' + url: 'index' + - title: 'Configuring Ember CLI' + url: 'configuring-ember-cli' + - title: 'Handling Deprecations' + url: 'handling-deprecations' + - title: 'Disabling Prototype Extensions' + url: 'disabling-prototype-extensions' + - title: 'Specifying the URL Type' + url: 'specifying-url-type' + - title: 'Embedding Applications' + url: 'embedding-applications' + - title: 'Feature Flags' + url: 'feature-flags' isAdvanced: true - - title: "Optional Features" - url: "optional-features" + - title: 'Optional Features' + url: 'optional-features' isAdvanced: true - - title: "Build targets" - url: "build-targets" - - title: "Debugging" - url: "debugging" -- title: "Testing" - url: "testing" + - title: 'Build targets' + url: 'build-targets' + - title: 'Debugging' + url: 'debugging' +- title: 'Testing' + url: 'testing' pages: - - title: "Introduction" - url: "index" - - title: "Testing Tools" - url: "testing-tools" - - title: "Test Types" - url: "test-types" - - title: "Testing Application" - url: "testing-application" - - title: "Testing Basics" - url: "unit-testing-basics" - - title: "Testing Components" - url: "testing-components" - - title: "Testing Helpers" - url: "testing-helpers" - - title: "Testing Controllers" - url: "testing-controllers" - - title: "Testing Routes" - url: "testing-routes" - - title: "Testing Models" - url: "testing-models" -- title: "Addons and Dependencies" - url: "addons-and-dependencies" + - title: 'Introduction' + url: 'index' + - title: 'Testing Tools' + url: 'testing-tools' + - title: 'Test Types' + url: 'test-types' + - title: 'Testing Application' + url: 'testing-application' + - title: 'Testing Basics' + url: 'unit-testing-basics' + - title: 'Testing Components' + url: 'testing-components' + - title: 'Testing Helpers' + url: 'testing-helpers' + - title: 'Testing Controllers' + url: 'testing-controllers' + - title: 'Testing Routes' + url: 'testing-routes' + - title: 'Testing Models' + url: 'testing-models' +- title: 'Addons and Dependencies' + url: 'addons-and-dependencies' pages: - - title: "Managing Dependencies" - url: "index" -- title: "Using TypeScript" - url: "using-typescript" + - title: 'Managing Dependencies' + url: 'index' +- title: 'Using TypeScript' + url: 'using-typescript' pages: - - title: "TypeScript: Introduction" - url: "index" - - title: "Getting Started with TypeScript" - url: "getting-started" - - title: "TypeScript Configuration" - url: "configuration" - - title: "TypeScript and Ember" - url: "ts-and-ember" -- title: "Developer Tools" - url: "toc-heading_developer-tools" + - title: 'TypeScript: Introduction' + url: 'index' + - title: 'Getting Started with TypeScript' + url: 'getting-started' + - title: 'TypeScript Configuration' + url: 'configuration' + - title: 'TypeScript and Ember' + url: 'ts-and-ember' + - title: 'TypeScript Patterns for Ember' + url: 'ts-patterns-for-ember' + - title: 'TypeScript Patterns for EmberData' + url: 'ts-patterns-for-ember-data' + - title: 'Building Addons in TypeScript' + url: 'building-addons-in-typescript' +- title: 'Developer Tools' + url: 'toc-heading_developer-tools' is_heading: true -- title: "Ember Inspector" - url: "ember-inspector" +- title: 'Ember Inspector' + url: 'ember-inspector' pages: - - title: "Introduction" - url: "index" - - title: "Installing the Inspector" - url: "installation" - - title: "Object Inspector" - url: "object-inspector" - - title: "The Component Tree" - url: "component-tree" - - title: "Inspecting Routes" - url: "routes" - - title: "Data Tab" - url: "data" - - title: "Tackling Deprecations" - url: "deprecations" - - title: "Library Info" - url: "info" - - title: "Debugging Promises" - url: "promises" - - title: "Inspecting Objects via the Container" - url: "container" - - title: "Rendering Performance" - url: "render-performance" - - title: "Troubleshooting" - url: "troubleshooting" -- title: "Code Editors" - url: "code-editors" + - title: 'Introduction' + url: 'index' + - title: 'Installing the Inspector' + url: 'installation' + - title: 'Object Inspector' + url: 'object-inspector' + - title: 'The Component Tree' + url: 'component-tree' + - title: 'Inspecting Routes' + url: 'routes' + - title: 'Data Tab' + url: 'data' + - title: 'Tackling Deprecations' + url: 'deprecations' + - title: 'Library Info' + url: 'info' + - title: 'Debugging Promises' + url: 'promises' + - title: 'Inspecting Objects via the Container' + url: 'container' + - title: 'Rendering Performance' + url: 'render-performance' + - title: 'Troubleshooting' + url: 'troubleshooting' +- title: 'Code Editors' + url: 'code-editors' pages: - - title: "Ember Extensions" - url: "index" + - title: 'Ember Extensions' + url: 'index' -- title: "Additional Resources" - url: "toc-heading_additional-resources" +- title: 'Additional Resources' + url: 'toc-heading_additional-resources' is_heading: true -- title: "Upgrading" - url: "upgrading" +- title: 'Upgrading' + url: 'upgrading' pages: - - title: "How to upgrade" - url: "index" - - title: "Octane Upgrade Guide" - url: "current-edition" + - title: 'How to upgrade' + url: 'index' + - title: 'Octane Upgrade Guide' + url: 'current-edition' pages: - - title: "Introduction" - url: "index" - - title: "Templates" - url: "templates" - - title: "Native Classes" - url: "native-classes" - - title: "Tracked Properties" - url: "tracked-properties" - - title: "@action, {{on}} and {{fn}}" - url: "action-on-and-fn" - - title: "Glimmer Components" - url: "glimmer-components" - - title: "Cheat Sheet" - url: "cheat-sheet" -- title: "Contributing to Ember.js" - url: "contributing" + - title: 'Introduction' + url: 'index' + - title: 'Templates' + url: 'templates' + - title: 'Native Classes' + url: 'native-classes' + - title: 'Tracked Properties' + url: 'tracked-properties' + - title: '@action, {{on}} and {{fn}}' + url: 'action-on-and-fn' + - title: 'Glimmer Components' + url: 'glimmer-components' + - title: 'Cheat Sheet' + url: 'cheat-sheet' +- title: 'Contributing to Ember.js' + url: 'contributing' isAdvanced: true pages: - - title: "Overview" - url: "index" - - title: "Adding New Features" - url: "adding-new-features" - - title: "Repositories" - url: "repositories" -- title: "Glossary" - url: "glossary" + - title: 'Overview' + url: 'index' + - title: 'Adding New Features' + url: 'adding-new-features' + - title: 'Repositories' + url: 'repositories' +- title: 'Glossary' + url: 'glossary' pages: - - title: "Web Development" - url: "index" + - title: 'Web Development' + url: 'index' diff --git a/guides/release/using-typescript/building-addons-in-typescript.md b/guides/release/using-typescript/building-addons-in-typescript.md new file mode 100644 index 0000000000..27a6e5391c --- /dev/null +++ b/guides/release/using-typescript/building-addons-in-typescript.md @@ -0,0 +1,79 @@ + + +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. + +## Key Differences from Apps + +To process `.ts` files, `ember-cli-typescript` tells Ember CLI to [register a set of Babel plugins](https://devblogs.microsoft.com/typescript/typescript-and-babel-7/) 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. + +- 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**. + +## Publishing + +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. + +## Linking 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 [`yarn link`](https://yarnpkg.com/en/docs/cli/link#search) or [`npm link`](https://docs.npmjs.com/cli/link). + +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 `` and `/*` in your `tsconfig.json` like so: + +```javascript +compilerOptions: { + // ...other options + paths: { + // ...other paths, e.g. for your app/ and tests/ trees + // resolve: import x from 'my-addon'; + "my-addon": [ + "node_modules/my-addon/addon" + ], + // resolve: import y from 'my-addon/utils/y'; + "my-addon/*": [ + "node_modules/my-addon/addon/*" + ] + } +} +``` + +## In-Repo Addons + +[In-repo addons](https://ember-cli.com/extending/#detailed-list-of-blueprints-and-their-use) 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. + +```javascript +compilerOptions: { + // ...other options + paths: { + // ...other paths, e.g. for your tests/ tree + "my-app": [ + "app/*", + // add addon app directory that will be merged with the host application + "lib/my-addon/app/*" + ], + // resolve: import x from 'my-addon'; + "my-addon": [ + "lib/my-addon/addon" + ], + // resolve: import y from 'my-addon/utils/y'; + "my-addon/*": [ + "lib/my-addon/addon/*" + ] + } +} +``` + +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. diff --git a/guides/release/using-typescript/index.md b/guides/release/using-typescript/index.md index 4da219a491..1af0abe483 100644 --- a/guides/release/using-typescript/index.md +++ b/guides/release/using-typescript/index.md @@ -2,7 +2,6 @@ This guide is designed to help you get up and running with TypeScript in an Ember app. -
@@ -28,6 +27,7 @@ To get started, check out the instructions in [Getting Started with TypeScript]( - If you're working with legacy (pre-Octane) Ember and TypeScript together, you should read [the Legacy Guide](./legacy/README.md). - Looking for type-checking in Glimmer templates? Check out [Glint](https://typed-ember.gitbook.io/glint/). + ## Why TypeScript? diff --git a/guides/release/using-typescript/ts-and-ember.md b/guides/release/using-typescript/ts-and-ember.md index e1b672b832..91dc14c8f8 100644 --- a/guides/release/using-typescript/ts-and-ember.md +++ b/guides/release/using-typescript/ts-and-ember.md @@ -1,7 +1,5 @@ **Note:** 🚧 This section is under construction! 🏗️ The content here may not be fully up to date! -# TypeScript and Ember - 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: - [TypeScript docs](https://www.typescriptlang.org/docs/index.html) @@ -166,7 +164,7 @@ This definition allows for type-safe lookups with other Ember dependency injecti ```typescript function dynamicLookup(owner: Owner) { let mySession = owner.lookup('service:my-session'); - mySession.login("tom@example.com", "password123"); + mySession.login('tom@example.com', 'password123'); } ``` @@ -284,3 +282,98 @@ This works because (a) we include things in your types directory automatically a 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! + + +# Decorators + +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: + +1. 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. + +2. 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! + +3. 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: + +- [Services](../ember/services.md) +- [Ember Data Models](../ember-data/models.md) + +# Current Limitations + +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 [existing typings](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/ember); others are a matter of further support landing in TypeScript itself, or changes to Ember's object model. + +## Some `import`s don't resolve + +You'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. + +Writing these missing type definitions is a great way to pitch in! Jump in `#topic-typescript` on the [Ember Community Discord server](https://discord.gg/zT3asNS) and we'll be happy to help you. + +## 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. + +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: + +```ts +declare module '*/template' { + import { TemplateFactory } from 'ember-cli-htmlbars'; + const template: TemplateFactory; + export default template; +} + +declare module 'app/templates/*' { + import { TemplateFactory } from 'ember-cli-htmlbars'; + const template: TemplateFactory; + export default template; +} + +declare module 'addon/templates/*' { + import { TemplateFactory } from 'ember-cli-htmlbars'; + const template: TemplateFactory; + export default template; +} +``` + +## Invoking actions + +TypeScript won't detect a mismatch between this action and the corresponding call in the template: + +```ts +import Component from '@ember/component'; +import { action } from '@ember/object'; + +export default class MyGame extends Component { + @action turnWheel(degrees: number) { + // ... + } +} +``` + +```hbs + +``` + +Likewise, it won't notice a problem when you use the `send` method: + +```ts +// TypeScript compiler won't detect this type mismatch +this.send\('turnWheel', 'ALSO-NOT-A-NUMBER'\); +``` + +# Understanding the `@types` Package Names + +You may be wondering why the packages added to your `package.json` and described in [**Installation: Other packages this addon installs**](https://github.com/typed-ember/ember-cli-typescript/tree/3a434def8b8c8214853cea0762940ccedb2256e8/docs/README.md#other-packages-this-addon-installs) 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 [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) publishing infrastructure \([types-publisher](https://github.com/Microsoft/types-publisher)\) to handle scoped packages, documented under [**What about scoped packages?**](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master#what-about-scoped-packages) in [the DefinitelyTyped README](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master). + +See also: + +- [Microsoft/types-publisher\#155](https://github.com/Microsoft/types-publisher/issues/155) +- [Microsoft/Typescript\#14819](https://github.com/Microsoft/TypeScript/issues/14819) diff --git a/guides/release/using-typescript/ts-patterns-for-ember-data.md b/guides/release/using-typescript/ts-patterns-for-ember-data.md new file mode 100644 index 0000000000..8f110ee879 --- /dev/null +++ b/guides/release/using-typescript/ts-patterns-for-ember-data.md @@ -0,0 +1,187 @@ +In this section, we cover how to use TypeScript effectively with specific Ember Data APIs \(anything you'd find under the `@ember-data` package namespace\). + +We do _not_ cover general usage of Ember Data; instead, we assume that as background knowledge. Please see the Ember Data [Guides](https://guides.emberjs.com/release/models) and [API docs](https://api.emberjs.com/ember-data/release)! + +# Models + +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](../ts/decorators.md). + +## `@attr` + +The type returned by the `@attr` decorator is whatever [Transform](https://api.emberjs.com/ember-data/release/classes/Transform) is applied via the invocation. See [our overview of Transforms for more information](./transforms.md). + +- 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: + +```typescript +import Model, { attr } from '@ember-data/model'; +import CustomType from '../transforms/custom-transform'; + +export default class User extends Model { + @attr() + declare name?: string; + + @attr('number') + declare age: number; + + @attr('boolean') + declare isAdmin: boolean; + + @attr('custom-transform') + declare myCustomThing: CustomType; +} +``` + +**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: + +```typescript +import Model, { attr } from '@ember-data/model'; + +export default class User extends Model { + @attr() + declare name?: string; + + @attr('number', { defaultValue: 13 }) + declare age: number; + + @attr('boolean', { defaultValue: false }) + declare isAdmin: boolean; +} +``` + +## Relationships + +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`](https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-cycle.md) error from eslint. + +To avoid these errors, use [type-only imports](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html), available since TypeScript 3.8: + +```ts +import type User from './user'; +``` + +### `@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`, 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: + +```typescript +import Model, { belongsTo, type AsyncBelongsTo } from '@ember-data/model'; +import type User from './user'; +import type Site from './site'; + +export default class Post extends Model { + @belongsTo('user') + declare user: AsyncBelongsTo; + + @belongsTo('site', { async: false }) + declare site: Site; +} +``` + +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` 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`, where `Model` is the type of the model you are creating a relationship to. +- If the value is `false`, the type is `SyncHasMany`, where `Model` is the type of the model you are creating a relationship to. + +So, for example, you might define a class like this: + +```typescript +import Model, { + hasMany, + type AsyncHasMany, + type SyncHasMany, +} from '@ember-data/model'; +import type Comment from './comment'; +import type User from './user'; + +export default class Thread extends Model { + @hasMany('comment') + declare comments: AsyncHasMany; + + @hasMany('user', { async: false }) + declare participants: SyncHasMany; +} +``` + +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. + +# Transforms + +In Ember Data, `attr` defines an attribute on a [Model](https://guides.emberjs.com/release/models/defining-models/). +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](https://guides.emberjs.com/release/models/defining-models/#toc_custom-transforms). +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: + +```typescript +# app/transforms/coordinate-point.ts +import Transform from '@ember-data/serializer/transform'; + +declare module 'ember-data/types/registries/transform' { + export default interface TransformRegistry { + 'coordinate-point': CoordinatePointTransform; + } +} + +export type CoordinatePoint = { + x: number; + y: number; +}; + +export default class CoordinatePointTransform extends Transform { + deserialize(serialized): CoordinatePoint { + return { x: value[0], y: value[1] }; + } + + serialize(value): number { + return [value.x, value.y]; + } +} + +# app/models/cursor.ts +import Model, { attr } from '@ember-data/model'; +import { CoordinatePoint } from 'agwa-data/transforms/coordinate-point'; + +declare module 'ember-data/types/registries/transform' { + export default interface ModelRegistry { + cursor: Cursor; + } +} + +export default class Cursor extends Model { + @attr('coordinate-point') declare position: CoordinatePoint; +} +``` + +Note that you should declare your own transform under `TransformRegistry` to make `attr` to work with your transform. diff --git a/guides/release/using-typescript/ts-patterns-for-ember.md b/guides/release/using-typescript/ts-patterns-for-ember.md new file mode 100644 index 0000000000..610104411c --- /dev/null +++ b/guides/release/using-typescript/ts-patterns-for-ember.md @@ -0,0 +1,1017 @@ +In this section, we cover how to use TypeScript effectively with specific Ember APIs \(anything you'd find under the `@ember` package namespace\). + +We do _not_ cover general usage of Ember; instead, we assume that as background knowledge. Please see the Ember [Guides](https://guides.emberjs.com/release/) and [API docs](https://api.emberjs.com/ember/release)! + +## Outline + +- [Components](./components.md) +- [Services](./services.md) +- [Routes](./routes.md) +- [Controllers](./controllers.md) +- [Helpers](./helpers.md) +- [Testing](./testing.md) + +# Components + +{% hint style="info" %} +New to Ember or the Octane edition specifically? You may want to read [the Ember Guides’ material on `Component`s](https://guides.emberjs.com/release/components/) first! +{% endhint %} + +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 [Glint](https://typed-ember.gitbook.io/glint/). + +## A simple component + +A _very_ simple Glimmer component which lets you change the count of a value might look like this: + +```text + +{{this.count}} + +``` + +```typescript +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; + +export default class Counter extends Component { + @tracked count = 0; + + @action plus() { + this.count += 1; + } + + @action minus() { + this.count -= 1; + } +} +``` + +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. + +## Adding arguments and giving them a type + +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. + +Since the implementation of [RFC 748], 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`. + +[rfc 748]: https://github.com/emberjs/rfcs/pull/748 + +`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: + +``` +export interface MySignature { + Args: { + arg1: string; + arg2: number; + arg3: boolean; + } +} +``` + +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 [Component Signatures documentation](https://typed-ember.gitbook.io/glint/using-glint/ember/component-signatures). + +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: + +```typescript +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 { + constructor(owner: unknown, args: ArgsDisplaySignature['Args]) { + super(owner, args); + + Object.keys(args).forEach(log); + } +} +``` + +{% hint style="info" %} +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` +- 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 [the JavaScript spec](https://tc39.es/ecma262/#sec-runtime-semantics-classdefinitionevaluation)\) + {% endhint %} + +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. + +{% hint style="info" %} +This might change in the future! If TypeScript eventually adds [support for “variadic kinds”](https://github.com/Microsoft/TypeScript/issues/5453), using `...arguments` could become safe. +{% endhint %} + +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. + +The `args` passed to a Glimmer Component [are available on `this`](https://github.com/glimmerjs/glimmer.js/blob/2f840309f013898289af605abffe7aee7acc6ed5/packages/%40glimmer/component/src/component.ts#L12), so we could change our definition to return the names of the arguments from a getter: + +```typescript +import Component from '@glimmer/component'; + +export interface ArgsDisplaySignature { + Args: { + arg1: string; + arg2: number; + arg3: boolean; + }; +} + +export default class ArgsDisplay extends Component { + get argNames(): string[] { + return Object.keys(this.args); + } +} +``` + +```text +

The names of the @args are:

+
    + {{#each this.argNames as |argName|}} +
  • {{argName}}
  • + {{/each}} +
+``` + +### Understanding `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: + +```typescript +export default class Component { + readonly args: Args; + + constructor(owner: unknown, args: Args); +} +``` + +{% hint style="info" %} +Not sure what’s up with `` _at all_? We highly recommend the [TypeScript Deep Dive](https://basarat.gitbooks.io/typescript/) book’s [chapter on generics](https://basarat.gitbooks.io/typescript/docs/types/generics.html) to be quite helpful in understanding this part. +{% endhint %} + +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` , where `T` is the type of thing in the array, which TypeScript normally figures out for you automatically at compile time: + +```typescript +let a = [1, 2, 3]; // Array +let b = ['hello', 'goodbye']; // Array +``` + +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` . 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`. + +### Giving `args` a type + +Now 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: + +```text + +``` + +Then we could capture the types for the profile with an interface representing the _arguments_: + +```typescript +import Component from '@glimmer/component'; +import { generateUrl } from '../lib/generate-avatar'; + +interface User { + name: string; + avatar?: string; + bio?: string; +} + +export default class UserProfile extends Component { + get userInfo(): string { + return this.args.bio + ? `${this.args.name} ${this.args.bio}` + : this.args.name; + } + + get avatar(): string { + return this.args.avatar ?? generateUrl(); + } +} +``` + +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. + +## Generic subclasses + +If you'd like to make your _own_ component subclass-able, you need to make it generic as well. + +{% hint style="warning" %} +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! +{% endhint %} + +```typescript +import Component from '@glimmer/component'; + +export interface FancyInputArgs { + // ... +} + +export default class FancyInput< + Args extends FancyInputArgs = FancyInputArgs +> extends Component { + // ... +} +``` + +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. + +# Services + +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. + +{% hint style="info" %} +If you are not familiar with Services in Ember, first make sure you have read and understood the [Ember Guide on Services](https://guides.emberjs.com/release/services/)! +{% endhint %} + +## A basic service + +Let's take this example from the [Ember Guide](https://guides.emberjs.com/release/services/): + +```typescript +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. + +{% hint style="info" %} +When working in Octane, you're better off using a `TrackedArray` from [tracked-built-ins](https://github.com/pzuraq/tracked-built-ins) instead of the classic EmberArray: + +```typescript +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. +{% endhint %} + +## 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: + +```typescript +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: + +```typescript +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: + +```typescript +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\). + +{% hint style="danger" %} +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:`\) 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. + +There is a merged \(but not yet implemented\) [RFC](https://emberjs.github.io/rfcs/0585-improved-ember-registry-apis.html) which improves this design and makes it straightforward to type-check. Additionally, TypeScript 4.1's introduction of [template types](https://devblogs.microsoft.com/typescript/announcing-typescript-4-1/#template-literal-types) may allow us to supply types that work with the microsyntax. + +For now, however, remember that _the cast is unsafe_! +{% endhint %} + +# Routes + +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: + +```typescript +import Route from '@ember/routing/route'; +import type RouterService from '@ember/routing/router-service'; +type Transition = ReturnType; + +export default class MyRoute extends Route { + beforeModel(transition: Transition) { + // ... + } +} +``` + +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 [Working with Route Models](../cookbook/working-with-route-models.md) for details! + +```typescript +import Route from '@ember/routing/route'; + +type Resolved

= P extends Promise ? T : P; + +export type MyRouteModel = Resolved>; + +export default class MyRoute extends Route { + model() { + // ... + } +} +``` + +The `Resolved` 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` 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. + +```typescript +import Controller from '@ember/controller'; +import type { MyRouteModel } from '../routes/my-route'; + +export default class MyController extends Controller { + declare model?: MyRouteModel; + + // ... +} +``` + +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 `` 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. + +# 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 some type utilities to let us get the resolved value returned by a route’s model hook: + +```typescript +import Route from '@ember/routing/route'; + +/** + Get the resolved type of an item. + + - If the item is a promise, the result will be the resolved value type + - If the item is not a promise, the result will just be the type of the item + */ +export type Resolved

= P extends Promise ? T : P; + +/** Get the resolved model value from a route. */ +export type ModelFrom = Resolved>; +``` + +How that works: + +- `Resolved

` says "if this is a promise, the type here is whatever the promise resolves to; otherwise, it's just the value" +- `ReturnType` 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` ends up giving you the resolved value returned from the `model` hook for a given route: + +```typescript +type MyRouteModel = ModelFrom; +``` + +## `model` on the controller + +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: + +```typescript +import Controller from '@ember/controller'; +import MyRoute from '../routes/my-route'; +import { ModelFrom } from '../lib/type-utils'; + +export default class ControllerWithModel extends Controller { + declare model: ModelFrom; +} +``` + +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. + +# Controllers + +Like [routes](./routes.md), controllers are just normal classes with a few special Ember lifecycle hooks and properties available. + +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: + +```typescript +import Controller from '@ember/controller'; + +export default class HeyoController extends Controller { + queryParams = [ + { + category: { type: 'array' }, + }, + ]; +} +``` + +Then you will see a type error like this: + +```text +Property 'queryParams' in type 'HeyoController' is not assignable to the same property in base type 'Controller'. + Type '{ category: { type: string; }; }[]' is not assignable to type '(string | Record)[]'. + Type '{ category: { type: string; }; }' is not assignable to type 'string | Record'. + Type '{ category: { type: string; }; }' is not assignable to type 'Record'. + 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: + + + +```typescript +import Controller from "@ember/controller"; + +export default class HeyoController extends Controller { + queryParams = [ + { +- category: { type: "array" }, ++ category: { type: "array" as const }, + }, + ]; +} +``` + +Now it will type-check. + +# Helpers + +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. + +{% hint style="info" %} +As always, you should start by reading and understanding the [Ember Guide on Helpers](https://guides.emberjs.com/release/templates/writing-helpers/)! +{% endhint %} + +## Function-based helpers + +The basic type of a helper function in Ember is: + +```typescript +type FunctionBasedHelper = ( + positional: unknown[], + named: Record +) => string | void; +``` + +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: + +1. `positional` is an array of `unknown`, of unspecified length. +2. `named` is a `Record`. +3. Both arguments are always set, but may be empty. + +Let’s walk through each of these. + +### Handling `positional` arguments + +The 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](https://microsoft.github.io/TypeScript-New-Handbook/chapters/narrowing/)—TypeScript’s way of using runtime checks to inform the types at runtime. + +```typescript +function totalLength(positional: unknown[]) { + // Account for case where user passes no arguments + assert( + 'all positional args to `total-length` must be strings', + positional.every((arg) => typeof arg === 'string') + ); + + // safety: we can cast `positional as string[]` because we asserted above + return (positional as string[]).reduce((sum, s) => sum + s.length, 0); +} +``` + +### Handling `named` arguments + +We specified the type of `named` as a `Record`. `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` presence + +Note 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: + +```typescript +import { helper } from '@ember/component/helper'; + +const describe = (entries: string): string => + entries.length > 0 ? entries : '(none)'; + +export function showAll(positional: unknown[], named: Record) { + // pretty print each item with its index, like `0: { neat: true }` or + // `1: undefined`. + const positionalEntries = positional + .reduce( + (items, arg, index) => items.concat(`${index}: ${JSON.stringify(arg)}`), + [] + ) + .join(', '); + + // pretty print each item with its name, like `cool: beans` or + // `answer: 42`. + const namedEntries = Object.keys(named) + .reduce( + (items, key) => + items.concat(`${key}: ${JSON.stringify(named[key], undefined, 2)}`), + [] + ) + .join(', '); + + return `positional: ${describe(positionalEntries)}\nnamed: ${describe( + namedEntries + )}`; +} + +export default helper(showAll); +``` + +### Putting it all together + +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: + +```typescript +import { helper } from '@ember/component/helper'; +import { assert } from '@ember/debug'; +import { is } from '../../type-utils'; + +export function join(positional: [unknown, unknown], named: Dict) { + assert( + `'join' requires two 'string' positional parameters`, + is<[string, string]>( + positional, + positional.length === 2 && + positional.every((el) => typeof el === 'string') + ) + ); + assert( + `'join' requires argument 'separator'`, + typeof named.separator === 'string' + ); + + const joined = positional.join(named.separator); + const prefix = typeof named.prefix === 'string' ? named.prefix : ''; + + return `${prefix}${joined}`; +} + +export default helper(join); +``` + +## Class-based helpers + +The basic type of a class-based helper function in Ember is: + +```typescript +interface ClassBasedHelper { + compute( + positional?: unknown[], + named?: Record + ): string | void; +} +``` + +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. + +```typescript +import Helper from '@ember/component/helper'; +import { inject as service } from '@ember/service'; +import Authentication from 'my-app/services/authentication'; + +export default class Greet extends Helper { + @service authentication: Authentication; + + compute() { + return this.authentication.isAuthenticated + ? `Welcome back, ${authentication.userName}!` + : 'Sign in?'; +} +``` + +For more details on using decorators, see our [guide to using decorators](https://github.com/typed-ember/ember-cli-typescript/tree/3a434def8b8c8214853cea0762940ccedb2256e8/docs/ember/%28../ts/decorators/%29/README.md). For details on using services, see our [guide to services](https://github.com/typed-ember/ember-cli-typescript/tree/3a434def8b8c8214853cea0762940ccedb2256e8/docs/ember/%28./services/%29/README.md). + +# Testing + +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 [Testing Guides](https://guides.emberjs.com/release/testing/) first. The rest of this guide assumes you're already comfortable with testing in Ember! + +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. + +## App tests + +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. + +{% hint style="info" %} +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 [API docs](https://api.emberjs.com/ember/3.14/functions/@ember%2Fdebug/assert)! It’s a development-and-test-only helper that gets stripped from production builds, and is very helpful for this kind of thing! +{% endhint %} + +```javascript +// app/utils/math.js + +export function add(a, b) { + assert( + 'arguments must be numbers', + typeof a === number && typeof b === number + ); + + return a + b; +} +``` + +Then the test for it might look something like this: + +```javascript +// tests/unit/utils/math-test.js + +import { module, test } from 'qunit'; +import { add } from 'app/utils/math'; + +module('the `add` function', function (hooks) { + test('adds numbers correctly', function (assert) { + assert.equal('2 + 2 is 4', add(2, 2), 4); + assert.notEqual('2 + 2 is a number', add(2, 2), NaN); + assert.notEqual('2 + 2 is not infinity', add(2, 2), Infinity); + }); + + test('throws an error with strings', function (assert) { + assert.throws('when the first is a string and the second is a number', () => + add('hello', 1) + ); + assert.throws('when the first is a number and the second is a string', () => + add(0, 'hello') + ); + assert.throws('when both are strings', () => add('hello', 'goodbye')); + }); +}); +``` + +In TypeScript, that wouldn't make any sense at all, because we'd simply add the types to the function declaration: + +```typescript +// app/utils/math.ts + +export function add(a: number, b: number): number { + assert( + 'arguments must be numbers', + typeof a === number && typeof b === number + ); + + return a + b; +} +``` + +We might still write tests to make sure what we actually got back was what we expected— + +```typescript +// tests/unit/utils/math-test.ts + +import { module, test } from 'qunit'; +import { add } from 'app/utils/math'; + +module('the `add` function', function (hooks) { + test('adds numbers correctly', function (assert) { + assert.equal('2 + 2 is 4', add(2, 2), 4); + assert.notEqual('2 + 2 is a number', add(2, 2), NaN); + assert.notEqual('2 + 2 is not infinity', add(2, 2), Infinity); + }); +}); +``` + +—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: + +```typescript +// app/utils/math.ts + +export function add(a: number, b: number): number { + return a + b; +} +``` + +## Addon tests + +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. + +```typescript +function add(a: number, b: number): number { + assert( + 'arguments must be numbers', + typeof a === number && typeof b === number + ); + + return a + b; +} +``` + +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! + +```javascript +// tests/unit/utils/math-test.js + +import { module, test } from 'qunit'; +import { add } from 'app/utils/math'; + +module('the `add` function', function(hooks) { + test('adds numbers correctly', function(assert) { + assert.equal('2 + 2 is 4', add(2, 2), 4); + assert.notEqual('2 + 2 is a number', add(2, 2), NaN); + assert.notEqual('2 + 2 is not infinity', add(2, 2), Infinity); + }); + + test('throws an error with strings', function(assert) { + assert.throws( + 'when the first is a string and the second is a number', + () => add('hello' as any, 1) + ); + assert.throws( + 'when the first is a number and the second is a string', + () => add(0, 'hello' as any) + ); + assert.throws( + 'when both are strings', + () => add('hello' as any, 'goodbye' as any) + ); + }) +}); +``` + +## Gotchas + +### The `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`: + +```typescript +// app/types/user.ts + +export default interface User { + displayName: string; + avatarUrl?: string; +} +``` + +Then our component might be defined like this: + +```text +{{! app/components/profile.hbs }} + +

+``` + +```typescript +import Component from '@glimmer/component'; +import User from 'app/types/user'; +import { randomAvatarURL } from 'app/utils/avatar'; + +export default class Profile extends Component { + get avatar() { + return this.args.avatar ?? randomAvatarURL(); + } + + get description() { + return this.args.avatar + ? `${this.args.displayName}'s custom profile picture` + : 'a randomly generated placeholder avatar'; + } +} +``` + +{% hint style="info" %} +Not familiar with how we define a Glimmer `Component` and its arguments? Check out [our guide](https://github.com/typed-ember/ember-cli-typescript/tree/3a434def8b8c8214853cea0762940ccedb2256e8/docs/ember/components/README.md)! +{% endhint %} + +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! + +```typescript +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +import User from 'app/types/user'; + +module('Integration | Component | Profile', function(hooks) { + setupRenderingTest(hooks); + + test('given a user with an avatar', async function(assert) { + this.user: User = { + displayName: 'Rey', + avatar: 'https://example.com/star-wars/rey', + }; + + await render(hbs` Date: Sat, 2 Sep 2023 12:55:10 -0700 Subject: [PATCH 05/63] Add under-construction notes --- .../release/using-typescript/building-addons-in-typescript.md | 2 ++ guides/release/using-typescript/ts-patterns-for-ember-data.md | 2 ++ guides/release/using-typescript/ts-patterns-for-ember.md | 2 ++ 3 files changed, 6 insertions(+) diff --git a/guides/release/using-typescript/building-addons-in-typescript.md b/guides/release/using-typescript/building-addons-in-typescript.md index 27a6e5391c..7148bf471b 100644 --- a/guides/release/using-typescript/building-addons-in-typescript.md +++ b/guides/release/using-typescript/building-addons-in-typescript.md @@ -1,3 +1,5 @@ +**Note:** 🚧 This section is under construction! 🏗️ The content here may not be fully up to date! + 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. diff --git a/guides/release/using-typescript/ts-patterns-for-ember-data.md b/guides/release/using-typescript/ts-patterns-for-ember-data.md index 8f110ee879..320fce1708 100644 --- a/guides/release/using-typescript/ts-patterns-for-ember-data.md +++ b/guides/release/using-typescript/ts-patterns-for-ember-data.md @@ -1,3 +1,5 @@ +**Note:** 🚧 This section is under construction! 🏗️ The content here may not be fully up to date! + In this section, we cover how to use TypeScript effectively with specific Ember Data APIs \(anything you'd find under the `@ember-data` package namespace\). We do _not_ cover general usage of Ember Data; instead, we assume that as background knowledge. Please see the Ember Data [Guides](https://guides.emberjs.com/release/models) and [API docs](https://api.emberjs.com/ember-data/release)! diff --git a/guides/release/using-typescript/ts-patterns-for-ember.md b/guides/release/using-typescript/ts-patterns-for-ember.md index 610104411c..c470ce516c 100644 --- a/guides/release/using-typescript/ts-patterns-for-ember.md +++ b/guides/release/using-typescript/ts-patterns-for-ember.md @@ -1,3 +1,5 @@ +**Note:** 🚧 This section is under construction! 🏗️ The content here may not be fully up to date! + In this section, we cover how to use TypeScript effectively with specific Ember APIs \(anything you'd find under the `@ember` package namespace\). We do _not_ cover general usage of Ember; instead, we assume that as background knowledge. Please see the Ember [Guides](https://guides.emberjs.com/release/) and [API docs](https://api.emberjs.com/ember/release)! From 4ff4476e9c5e2018043486a4cea4dca35518bb3b Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Sat, 2 Sep 2023 13:23:49 -0700 Subject: [PATCH 06/63] Adjust h# for pages that were combined from ember-cli-ts docs --- .../release/using-typescript/ts-and-ember.md | 38 +++++++------- .../ts-patterns-for-ember-data.md | 16 +++--- .../using-typescript/ts-patterns-for-ember.md | 50 +++++++++---------- 3 files changed, 52 insertions(+), 52 deletions(-) diff --git a/guides/release/using-typescript/ts-and-ember.md b/guides/release/using-typescript/ts-and-ember.md index 91dc14c8f8..c4f71c2d93 100644 --- a/guides/release/using-typescript/ts-and-ember.md +++ b/guides/release/using-typescript/ts-and-ember.md @@ -16,9 +16,9 @@ This guide covers the common details and "gotchas" of using TypeScript with Embe -# Using TypeScript With Ember Effectively +## Using TypeScript With Ember Effectively -## Incremental adoption +### Incremental adoption 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! @@ -53,7 +53,7 @@ Finally, leave `"noEmitOnError": true` (the default) in the `"compilerOptions"` ![example of a build error during live reload](https://user-images.githubusercontent.com/108688/38774630-7d9224d4-403b-11e8-8dbc-87dad977a4c4.gif) -## What about missing types? +### What about missing types? There are two schools of thought on how to handle things you don't have types for as you go: @@ -63,11 +63,11 @@ There are two schools of thought on how to handle things you don't have types fo There is an inherent tradeoff between these two approaches; which works best will depend on your team and your app. -## Install other types +### Install other types You'll want to use other type definitions as much as possible. Many packages ship their own type definitions, and many others have community-maintained definitions from [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped), available in the `@types` name space. The first thing you should do is to 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. -## The `types` directory +### The `types` directory During 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: @@ -77,31 +77,31 @@ During installation, we create a `types` directory in the root of your applicati These are all fallbacks, of course, you should use the types supplied directly with a package -### Global types for your package +#### Global types for your package At the root of your application or addon, we include a `types/` 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](https://guides.emberjs.com/v2.18.0/configuring-ember/disabling-prototype-extensions/), you can remove these declarations entirely; we include them because they're enabled in most Ember applications today. -### Environment configuration typings +#### Environment configuration typings 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 `/config/environment` into your build for consumption at runtime. -## String-keyed lookups +### String-keyed lookups 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: -### Classic `get` or `set` methods +#### Classic `get` or `set` methods In general, the `this.get` and `this.set` methods on `EmberObject` subclasses and the standalone `get` and `set` functions will work as you'd expect _if_ you're doing lookups only a single layer deep. We do not provide support for deep key lookups like `get(someObj, 'a.b.c')`, because normal property access can works correctly across the whole Ember ecosystem since at least Ember and Ember Data 3.28. Since regular property access “just works”, and has for a very long time, you should migrate to using normal property access instead. TypeScript will help make this a smooth process by identifying where you need to handle null and undefined intermediate properties. -### Service and controller injections +#### Service and controller injections Ember looks up services with the `@service` decorator at runtime, using the name of the service 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 to inject isn't available at compile time in the same way. (These same considerations apply to controller injections using the `@inject` decorator from `@ember/controller`.) @@ -168,7 +168,7 @@ function dynamicLookup(owner: Owner) { } ``` -### Ember Data lookups +#### Ember Data lookups We use the same basic approach for Ember Data type lookups with string keys as we do for service injections, but here we take advantage of the string "type registration" for the runtime code as well. 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. That is, writing `this.store.findRecord('user', 1)` will give you back a `Promise`. @@ -230,7 +230,7 @@ The declarations and changes you need to add to your existing files are: } ``` -#### Opt-in unsafety +##### Opt-in unsafety 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: @@ -258,7 +258,7 @@ declare module 'ember-data/types/registries/serializer' { 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! -#### Fixing the Ember Data `error TS2344` problem +##### Fixing the Ember Data `error TS2344` problem If 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: @@ -284,7 +284,7 @@ If you're developing an addon and concerned that this might affect consumers, it -# Decorators +## Decorators Ember makes heavy use of decorators, and TypeScript does not currently support deriving type information from decorators. @@ -305,17 +305,17 @@ For examples, see the detailed discussions of the two main places decorators are - [Services](../ember/services.md) - [Ember Data Models](../ember-data/models.md) -# Current Limitations +## Current Limitations 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 [existing typings](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/ember); others are a matter of further support landing in TypeScript itself, or changes to Ember's object model. -## Some `import`s don't resolve +### Some `import`s don't resolve You'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. Writing these missing type definitions is a great way to pitch in! Jump in `#topic-typescript` on the [Ember Community Discord server](https://discord.gg/zT3asNS) and we'll be happy to help you. -## Templates +### 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. @@ -341,7 +341,7 @@ declare module 'addon/templates/*' { } ``` -## Invoking actions +### Invoking actions TypeScript won't detect a mismatch between this action and the corresponding call in the template: @@ -369,7 +369,7 @@ Likewise, it won't notice a problem when you use the `send` method: this.send\('turnWheel', 'ALSO-NOT-A-NUMBER'\); ``` -# Understanding the `@types` Package Names +## Understanding the `@types` Package Names You may be wondering why the packages added to your `package.json` and described in [**Installation: Other packages this addon installs**](https://github.com/typed-ember/ember-cli-typescript/tree/3a434def8b8c8214853cea0762940ccedb2256e8/docs/README.md#other-packages-this-addon-installs) 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 [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) publishing infrastructure \([types-publisher](https://github.com/Microsoft/types-publisher)\) to handle scoped packages, documented under [**What about scoped packages?**](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master#what-about-scoped-packages) in [the DefinitelyTyped README](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master). diff --git a/guides/release/using-typescript/ts-patterns-for-ember-data.md b/guides/release/using-typescript/ts-patterns-for-ember-data.md index 320fce1708..81c86a092a 100644 --- a/guides/release/using-typescript/ts-patterns-for-ember-data.md +++ b/guides/release/using-typescript/ts-patterns-for-ember-data.md @@ -4,13 +4,13 @@ In this section, we cover how to use TypeScript effectively with specific Ember We do _not_ cover general usage of Ember Data; instead, we assume that as background knowledge. Please see the Ember Data [Guides](https://guides.emberjs.com/release/models) and [API docs](https://api.emberjs.com/ember-data/release)! -# Models +## Models 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](../ts/decorators.md). -## `@attr` +### `@attr` The type returned by the `@attr` decorator is whatever [Transform](https://api.emberjs.com/ember-data/release/classes/Transform) is applied via the invocation. See [our overview of Transforms for more information](./transforms.md). @@ -64,7 +64,7 @@ export default class User extends Model { } ``` -## Relationships +### Relationships 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`](https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-cycle.md) error from eslint. @@ -74,7 +74,7 @@ To avoid these errors, use [type-only imports](https://www.typescriptlang.org/do import type User from './user'; ``` -### `@belongsTo` +#### `@belongsTo` The type returned by the `@belongsTo` decorator depends on whether the relationship is `{ async: true }` \(which it is by default\). @@ -104,7 +104,7 @@ These are _type_-safe to define as always present, that is to leave off the `?` 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` +#### `@hasMany` The type returned by the `@hasMany` decorator depends on whether the relationship is `{ async: true }` \(which it is by default\). @@ -133,7 +133,7 @@ export default class Thread extends Model { 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. -# Transforms +## Transforms In Ember Data, `attr` defines an attribute on a [Model](https://guides.emberjs.com/release/models/defining-models/). By default, attributes are passed through as-is, however you can specify an @@ -147,7 +147,7 @@ The return type of `deserialize` method becomes type of the model class property You may define your own transforms in TypeScript like so: ```typescript -# app/transforms/coordinate-point.ts +## app/transforms/coordinate-point.ts import Transform from '@ember-data/serializer/transform'; declare module 'ember-data/types/registries/transform' { @@ -171,7 +171,7 @@ export default class CoordinatePointTransform extends Transform { } } -# app/models/cursor.ts +## app/models/cursor.ts import Model, { attr } from '@ember-data/model'; import { CoordinatePoint } from 'agwa-data/transforms/coordinate-point'; diff --git a/guides/release/using-typescript/ts-patterns-for-ember.md b/guides/release/using-typescript/ts-patterns-for-ember.md index c470ce516c..02a669b263 100644 --- a/guides/release/using-typescript/ts-patterns-for-ember.md +++ b/guides/release/using-typescript/ts-patterns-for-ember.md @@ -13,7 +13,7 @@ We do _not_ cover general usage of Ember; instead, we assume that as background - [Helpers](./helpers.md) - [Testing](./testing.md) -# Components +## Components {% hint style="info" %} New to Ember or the Octane edition specifically? You may want to read [the Ember Guides’ material on `Component`s](https://guides.emberjs.com/release/components/) first! @@ -21,7 +21,7 @@ New to Ember or the Octane edition specifically? You may want to read [the Ember 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 [Glint](https://typed-ember.gitbook.io/glint/). -## A simple component +### A simple component A _very_ simple Glimmer component which lets you change the count of a value might look like this: @@ -51,7 +51,7 @@ export default class Counter extends Component { 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. -## Adding arguments and giving them a type +### Adding arguments and giving them a type 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. @@ -143,7 +143,7 @@ export default class ArgsDisplay extends Component { ``` -### Understanding `args` +#### Understanding `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: @@ -174,7 +174,7 @@ let b = ['hello', 'goodbye']; // Array 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` . 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`. -### Giving `args` a type +#### Giving `args` a type Now 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: @@ -214,7 +214,7 @@ export default class UserProfile extends Component { 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. -## Generic subclasses +### Generic subclasses If you'd like to make your _own_ component subclass-able, you need to make it generic as well. @@ -238,7 +238,7 @@ export default class FancyInput< 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. -# Services +## Services 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. @@ -246,7 +246,7 @@ Ember Services are global singleton classes that can be made available to differ If you are not familiar with Services in Ember, first make sure you have read and understood the [Ember Guide on Services](https://guides.emberjs.com/release/services/)! {% endhint %} -## A basic service +### A basic service Let's take this example from the [Ember Guide](https://guides.emberjs.com/release/services/): @@ -303,7 +303,7 @@ export default class ShoppingCartService extends Service { Notice that here we are using only built-in array operations, not Ember's custom array methods. {% endhint %} -## Using services +### 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. @@ -379,7 +379,7 @@ There is a merged \(but not yet implemented\) [RFC](https://emberjs.github.io/rf For now, however, remember that _the cast is unsafe_! {% endhint %} -# Routes +## Routes 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. @@ -432,7 +432,7 @@ export default class MyController extends 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 `` 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. -# Working with route models +## 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! @@ -465,7 +465,7 @@ Putting those all together, `ModelFrom` ends up giving you the resolved v type MyRouteModel = ModelFrom; ``` -## `model` on the controller +### `model` on the controller 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: @@ -483,7 +483,7 @@ Now, our controller’s `model` property will _always_ stay in sync with the cor **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. -# Controllers +## Controllers Like [routes](./routes.md), controllers are just normal classes with a few special Ember lifecycle hooks and properties available. @@ -534,7 +534,7 @@ export default class HeyoController extends Controller { Now it will type-check. -# Helpers +## Helpers 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. @@ -542,7 +542,7 @@ Helpers in Ember are just functions or classes with a well-defined interface, wh As always, you should start by reading and understanding the [Ember Guide on Helpers](https://guides.emberjs.com/release/templates/writing-helpers/)! {% endhint %} -## Function-based helpers +### Function-based helpers The basic type of a helper function in Ember is: @@ -563,7 +563,7 @@ There are three important points about this definition: Let’s walk through each of these. -### Handling `positional` arguments +#### Handling `positional` arguments The 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](https://microsoft.github.io/TypeScript-New-Handbook/chapters/narrowing/)—TypeScript’s way of using runtime checks to inform the types at runtime. @@ -580,13 +580,13 @@ function totalLength(positional: unknown[]) { } ``` -### Handling `named` arguments +#### Handling `named` arguments We specified the type of `named` as a `Record`. `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` presence +#### `positional` and `named` presence Note 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: @@ -624,7 +624,7 @@ export function showAll(positional: unknown[], named: Record) { export default helper(showAll); ``` -### Putting it all together +#### Putting it all together 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: @@ -656,7 +656,7 @@ export function join(positional: [unknown, unknown], named: Dict) { export default helper(join); ``` -## Class-based helpers +### Class-based helpers The basic type of a class-based helper function in Ember is: @@ -688,13 +688,13 @@ export default class Greet extends Helper { For more details on using decorators, see our [guide to using decorators](https://github.com/typed-ember/ember-cli-typescript/tree/3a434def8b8c8214853cea0762940ccedb2256e8/docs/ember/%28../ts/decorators/%29/README.md). For details on using services, see our [guide to services](https://github.com/typed-ember/ember-cli-typescript/tree/3a434def8b8c8214853cea0762940ccedb2256e8/docs/ember/%28./services/%29/README.md). -# Testing +## Testing 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 [Testing Guides](https://guides.emberjs.com/release/testing/) first. The rest of this guide assumes you're already comfortable with testing in Ember! 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. -## App tests +### App tests 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. @@ -786,7 +786,7 @@ export function add(a: number, b: number): number { } ``` -## Addon tests +### Addon tests 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! @@ -837,9 +837,9 @@ module('the `add` function', function(hooks) { }); ``` -## Gotchas +### Gotchas -### The `TestContext` +#### The `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. From 71baee2d3c3c191e60387088f4a099b8eaae81d6 Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Sat, 2 Sep 2023 14:28:22 -0700 Subject: [PATCH 07/63] Clean up code-block formatting --- .../building-addons-in-typescript.md | 12 +- .../release/using-typescript/configuration.md | 8 +- .../using-typescript/getting-started.md | 4 +- .../release/using-typescript/ts-and-ember.md | 37 +++--- .../ts-patterns-for-ember-data.md | 13 +-- .../using-typescript/ts-patterns-for-ember.md | 106 ++++++++---------- 6 files changed, 85 insertions(+), 95 deletions(-) diff --git a/guides/release/using-typescript/building-addons-in-typescript.md b/guides/release/using-typescript/building-addons-in-typescript.md index 7148bf471b..68726c440c 100644 --- a/guides/release/using-typescript/building-addons-in-typescript.md +++ b/guides/release/using-typescript/building-addons-in-typescript.md @@ -35,10 +35,10 @@ You could run `ember ts:precompile` in your addon any time you change a file, bu Add entries for `` and `/*` in your `tsconfig.json` like so: -```javascript -compilerOptions: { +```json {data-filename="tsconfig.json"} +"compilerOptions": { // ...other options - paths: { + "paths": { // ...other paths, e.g. for your app/ and tests/ trees // resolve: import x from 'my-addon'; "my-addon": [ @@ -56,10 +56,10 @@ compilerOptions: { [In-repo addons](https://ember-cli.com/extending/#detailed-list-of-blueprints-and-their-use) 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. -```javascript -compilerOptions: { +```json {data-filename="tsconfig.json"} +"compilerOptions": { // ...other options - paths: { + "paths": { // ...other paths, e.g. for your tests/ tree "my-app": [ "app/*", diff --git a/guides/release/using-typescript/configuration.md b/guides/release/using-typescript/configuration.md index cdb3b617c3..6119eefdca 100644 --- a/guides/release/using-typescript/configuration.md +++ b/guides/release/using-typescript/configuration.md @@ -1,5 +1,7 @@ **Note:** 🚧 This section is under construction! 🏗️ The content here may not be fully up to date! + + ## `tsconfig.json` We generate a good default [`tsconfig.json`](https://github.com/typed-ember/ember-cli-typescript/blob/master/blueprint-files/ember-cli-typescript/tsconfig.json), which will usually make everything _Just Work™_. In general, you may customize your TypeScript build process as usual using the `tsconfig.json` file. @@ -18,7 +20,7 @@ However, there are a few things worth noting if you're already familiar with Typ To enable TypeScript sourcemaps, you'll need to add the corresponding configuration for Babel to your `ember-cli-build.js` file: -```typescript +```javascript {data-filename="ember-cli-build.js" data-diff="+2,+3,+4"} const app = new EmberApp(defaults, { babel: { sourceMaps: 'inline', @@ -30,7 +32,7 @@ const app = new EmberApp(defaults, { If you are using [Embroider](https://github.com/embroider-build/embroider), you might need to include [devtool](https://webpack.js.org/configuration/devtool/) in your webpack configuration: -```ts +```javascript {data-filename="ember-cli-build.js" data-diff="+4"} return require('@embroider/compat').compatBuild(app, Webpack, { packagerOptions: { webpackConfig: { @@ -40,4 +42,4 @@ return require('@embroider/compat').compatBuild(app, Webpack, { } ``` -If you're migrating from `ember-cli-typescript`, particularly an older version, to Ember's out-of-the-box TypeScript support, you may also need to update your `tsconfig.json`. Current versions of `ember-cli-typescript` generate the correct config at installation. You do *not* need to install `ember-cli-typescript` for new apps or adding. +If you're migrating from `ember-cli-typescript`, particularly an older version, to Ember's out-of-the-box TypeScript support, you may also need to update your `tsconfig.json`. Current versions of `ember-cli-typescript` generate the correct config at installation. You do _not_ need to install `ember-cli-typescript` for new apps or adding. diff --git a/guides/release/using-typescript/getting-started.md b/guides/release/using-typescript/getting-started.md index 93ae798d8c..01cda0dfb5 100644 --- a/guides/release/using-typescript/getting-started.md +++ b/guides/release/using-typescript/getting-started.md @@ -1,8 +1,10 @@ **Note:** 🚧 This section is under construction! 🏗️ The content here may not be fully up to date! + + To start a new Ember project with TypeScript, you can add the `--typescript` flag when you run [`ember new`](../../getting-started/quick-start): -```bash +```shell ember new my-typescript-app --typescript ``` diff --git a/guides/release/using-typescript/ts-and-ember.md b/guides/release/using-typescript/ts-and-ember.md index c4f71c2d93..99ed8fd299 100644 --- a/guides/release/using-typescript/ts-and-ember.md +++ b/guides/release/using-typescript/ts-and-ember.md @@ -18,6 +18,8 @@ This guide covers the common details and "gotchas" of using TypeScript with Embe ## Using TypeScript With Ember Effectively + + ### Incremental adoption 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! @@ -28,7 +30,7 @@ First, use the _strictest_ strictness settings that our typings allow (currently The full recommended _strictness_ settings in your `"compilerOptions"` hash (which are also the settings generated by the ember-cli-typescript blueprint): -```json +```json {data-filename="tsconfig.json"} { "compilerOptions": { // Strictness settings -- you should *not* change these: Ember code is not @@ -107,8 +109,7 @@ Ember looks up services with the `@service` decorator at runtime, using the name Since decorators do not currently have access to enough information to produce an appropriate type by themselves, we need to import and name the type explicitly. For example, we might have `MySession` service which defines a `login` method, defined as usual: -```typescript -// my-app/services/my-session.ts +```typescript {data-filename="app/services/my-session.ts"} import Service from '@ember/service'; import RSVP from 'rsvp'; @@ -127,8 +128,7 @@ declare module '@ember/service' { 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: -```typescript -// my-app/components/user-profile.ts +```typescript {data-filename="app/components/user-profile.ts"} import Component from '@ember/component'; import { inject as service } from '@ember/service'; @@ -151,7 +151,8 @@ Also notice [the `declare` property modifier](https://www.typescriptlang.org/doc Finally, you may have noticed the `declare module` at the bottom of the example `MySession` definition: -```typescript +```typescript {data-filename="app/services/my-session.ts"} +// ... declare module '@ember/service' { interface Registry { 'my-session': MySession; @@ -176,7 +177,7 @@ The declarations and changes you need to add to your existing files are: - Models - ```typescript + ```typescript {data-filename="app/models/user-meta.ts"} import Model from '@ember-data/model'; export default class UserMeta extends Model {} @@ -190,7 +191,7 @@ The declarations and changes you need to add to your existing files are: - Adapters - ```typescript + ```typescript {data-filename="app/adapters/user-meta.ts"} import Adapter from '@ember-data/adapter'; export default class UserMeta extends Adapter {} @@ -204,7 +205,7 @@ The declarations and changes you need to add to your existing files are: - Serializers - ```typescript + ```typescript {data-filename="app/serializers/user-meta.ts"} import Serializer from '@ember-data/serializer'; export default class UserMeta extends Serializer {} @@ -218,7 +219,7 @@ The declarations and changes you need to add to your existing files are: - Transforms - ```typescript + ```typescript {data-filename="app/transforms/color.ts"} import Transform from '@ember-data/serializer/transform'; export default class ColorTransform extends Transform {} @@ -234,7 +235,7 @@ The declarations and changes you need to add to your existing files are: 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: -```typescript +```typescript {data-filename="types/ember-data.d.ts"} import Model from '@ember-data/model'; import Adapter from '@ember-data/adapter'; import Serializer from '@ember-data/serializer'; @@ -270,7 +271,7 @@ This happens because the types for Ember's _test_ tooling includes the types for **The fix:** add a declaration like this in a new file named `ember-data.d.ts` in your `types` directory: -```typescript +```typescript {data-filename="types/ember-data.d.ts"} declare module 'ember-data/types/registries/model' { export default interface ModelRegistry { [key: string]: unknown; @@ -319,9 +320,11 @@ Writing these missing type definitions is a great way to pitch in! Jump in `#top 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: -```ts +```typescript {data-filename="my-addon/types/global.d.ts"} declare module '*/template' { import { TemplateFactory } from 'ember-cli-htmlbars'; const template: TemplateFactory; @@ -343,9 +346,11 @@ declare module 'addon/templates/*' { ### Invoking actions + + TypeScript won't detect a mismatch between this action and the corresponding call in the template: -```ts +```typescript {data-filename="app/components/my-game.ts"} import Component from '@ember/component'; import { action } from '@ember/object'; @@ -356,7 +361,7 @@ export default class MyGame extends Component { } ``` -```hbs +```handlebars {data-filename="app/components/my-game.hbs"} @@ -364,7 +369,7 @@ export default class MyGame extends Component { Likewise, it won't notice a problem when you use the `send` method: -```ts +```typescript // TypeScript compiler won't detect this type mismatch this.send\('turnWheel', 'ALSO-NOT-A-NUMBER'\); ``` diff --git a/guides/release/using-typescript/ts-patterns-for-ember-data.md b/guides/release/using-typescript/ts-patterns-for-ember-data.md index 81c86a092a..9d14305920 100644 --- a/guides/release/using-typescript/ts-patterns-for-ember-data.md +++ b/guides/release/using-typescript/ts-patterns-for-ember-data.md @@ -24,7 +24,7 @@ The type returned by the `@attr` decorator is whatever [Transform](https://api.e So, for example, you might write a class like this: -```typescript +```typescript {data-filename="app/models/user.ts"} import Model, { attr } from '@ember-data/model'; import CustomType from '../transforms/custom-transform'; @@ -49,7 +49,7 @@ The _safest_ type you can write for an Ember Data model, therefore, leaves every One way to make this safer is to supply a default value using the `defaultValue` on the options hash for the attribute: -```typescript +```typescript {data-filename="app/models/user.ts"} import Model, { attr } from '@ember-data/model'; export default class User extends Model { @@ -70,7 +70,7 @@ Relationships between models in Ember Data rely on importing the related models, To avoid these errors, use [type-only imports](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html), available since TypeScript 3.8: -```ts +```typescript import type User from './user'; ``` @@ -83,7 +83,7 @@ The type returned by the `@belongsTo` decorator depends on whether the relations So, for example, you might define a class like this: -```typescript +```typescript {data-filename="app/models/post.ts"} import Model, { belongsTo, type AsyncBelongsTo } from '@ember-data/model'; import type User from './user'; import type Site from './site'; @@ -113,7 +113,7 @@ The type returned by the `@hasMany` decorator depends on whether the relationshi So, for example, you might define a class like this: -```typescript +```typescript {data-filename="app/models/thread.ts"} import Model, { hasMany, type AsyncHasMany, @@ -146,8 +146,7 @@ The return type of `deserialize` method becomes type of the model class property You may define your own transforms in TypeScript like so: -```typescript -## app/transforms/coordinate-point.ts +```typescript {data-filename="app/transforms/coordinate-point.ts"} import Transform from '@ember-data/serializer/transform'; declare module 'ember-data/types/registries/transform' { diff --git a/guides/release/using-typescript/ts-patterns-for-ember.md b/guides/release/using-typescript/ts-patterns-for-ember.md index 02a669b263..0fcd804849 100644 --- a/guides/release/using-typescript/ts-patterns-for-ember.md +++ b/guides/release/using-typescript/ts-patterns-for-ember.md @@ -25,13 +25,13 @@ Glimmer Components are defined in one of three ways: with templates only, with a A _very_ simple Glimmer component which lets you change the count of a value might look like this: -```text - +```handlebars {data-filename="app/components/counter.hbs"} + {{this.count}} - + ``` -```typescript +```typescript {data-filename="app/components/counter.ts"} import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; @@ -63,13 +63,13 @@ Since the implementation of [RFC 748], Glimmer and Ember components accept a `Si `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: -``` +```typescript export interface MySignature { Args: { arg1: string; arg2: number; arg3: boolean; - } + }; } ``` @@ -77,7 +77,7 @@ If no `Args` key is specified, it will be a type error to pass any arguments to 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: -```typescript +```typescript {data-filename="app/components/args-display.ts"} import Component from '@glimmer/component'; const log = console.log.bind(console); @@ -116,7 +116,7 @@ The types for `owner` here and `args` line up with what the `constructor` for Gl The `args` passed to a Glimmer Component [are available on `this`](https://github.com/glimmerjs/glimmer.js/blob/2f840309f013898289af605abffe7aee7acc6ed5/packages/%40glimmer/component/src/component.ts#L12), so we could change our definition to return the names of the arguments from a getter: -```typescript +```typescript {data-filename="app/components/args-display.ts"} import Component from '@glimmer/component'; export interface ArgsDisplaySignature { @@ -134,7 +134,7 @@ export default class ArgsDisplay extends Component { } ``` -```text +```handlebars {data-filename="app/components/args-display.hbs"}

The names of the @args are:

    {{#each this.argNames as |argName|}} @@ -178,10 +178,10 @@ In the case of the Component, we have the types the way we do so that you can’ Now 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: -```text +```handlebars {data-filename="app/components/user-profile.hbs"} @@ -189,7 +189,7 @@ Now let’s put this to use. Imagine we’re constructing a user profile compone Then we could capture the types for the profile with an interface representing the _arguments_: -```typescript +```typescript {data-filename="app/components/user-profile.ts"} import Component from '@glimmer/component'; import { generateUrl } from '../lib/generate-avatar'; @@ -222,7 +222,7 @@ If you'd like to make your _own_ component subclass-able, you need to make it ge 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! {% endhint %} -```typescript +```typescript {data-filename="app/components/fancy-input-args.ts"} import Component from '@glimmer/component'; export interface FancyInputArgs { @@ -250,7 +250,7 @@ If you are not familiar with Services in Ember, first make sure you have read an Let's take this example from the [Ember Guide](https://guides.emberjs.com/release/services/): -```typescript +```typescript {data-filename="app/services/shopping-cart.ts"} import { A } from '@ember/array'; import Service from '@ember/service'; @@ -276,7 +276,7 @@ Just making this a TypeScript file gives us some type safety without having to a {% hint style="info" %} When working in Octane, you're better off using a `TrackedArray` from [tracked-built-ins](https://github.com/pzuraq/tracked-built-ins) instead of the classic EmberArray: -```typescript +```typescript {data-filename="app/services/shopping-cart.ts"} import { TrackedArray } from 'tracked-built-ins'; import Service from '@ember/service'; @@ -309,7 +309,7 @@ You can use a service in any container-resolved object such as a component or an Here's an example of using the `ShoppingCartService` we defined above in a component: -```typescript +```typescript {data-filename="app/components/cart-contents.ts"} import Component from '@glimmer/component'; import { inject as service } from '@ember/service'; import { action } from '@ember/object'; @@ -328,7 +328,7 @@ export default class CartContentsComponent extends Component { Any attempt to access a property or method not defined on the service will fail type-checking: -```typescript +```typescript {data-filename="app/components/cart-contents.ts"} import Component from '@glimmer/component'; import { inject as service } from '@ember/service'; import { action } from '@ember/object'; @@ -348,7 +348,7 @@ export default class CartContentsComponent extends Component { Services can also be loaded from the dependency injection container manually: -```typescript +```typescript {data-filename="app/components/cart-contents.ts"} import Component from '@glimmer/component'; import { getOwner } from '@ember/owner'; import { action } from '@ember/object'; @@ -387,7 +387,7 @@ However, there is one thing to watch out for: the types of the arguments passed 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: -```typescript +```typescript {data-filename="app/routes/my.ts"} import Route from '@ember/routing/route'; import type RouterService from '@ember/routing/router-service'; type Transition = ReturnType; @@ -401,7 +401,7 @@ export default class MyRoute extends Route { 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 [Working with Route Models](../cookbook/working-with-route-models.md) for details! -```typescript +```typescript {data-filename="app/routes/my.ts"} import Route from '@ember/routing/route'; type Resolved

    = P extends Promise ? T : P; @@ -419,7 +419,7 @@ The `Resolved` utility type takes in any type, and if the type is a `Promise` This in turn allows us to use the route class to define the type of the model on an associated controller. -```typescript +```typescript {data-filename="app/controllers/my.ts"} import Controller from '@ember/controller'; import type { MyRouteModel } from '../routes/my-route'; @@ -438,7 +438,7 @@ We often use routes’ models throughout our application, since they’re a core We can start by defining some type utilities to let us get the resolved value returned by a route’s model hook: -```typescript +```typescript {data-filename="app/lib/type-utils.ts"} import Route from '@ember/routing/route'; /** @@ -469,10 +469,10 @@ type MyRouteModel = ModelFrom; 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: -```typescript +```typescript {data-filename="app/controllers/controller-with-model.ts"} import Controller from '@ember/controller'; -import MyRoute from '../routes/my-route'; -import { ModelFrom } from '../lib/type-utils'; +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; @@ -489,7 +489,7 @@ Like [routes](./routes.md), controllers are just normal classes with a few speci 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: -```typescript +```typescript {data-filename="app/controllers/heyo.ts"} import Controller from '@ember/controller'; export default class HeyoController extends Controller { @@ -517,16 +517,14 @@ Property 'queryParams' in type 'HeyoController' is not assignable to the same pr 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: - - -```typescript -import Controller from "@ember/controller"; +```typescript {data-filename="app/controllers/heyo.ts", data-diff="-6,+7"} +import Controller from '@ember/controller'; export default class HeyoController extends Controller { queryParams = [ { -- category: { type: "array" }, -+ category: { type: "array" as const }, + category: { type: 'array' }, + category: { type: 'array' as const }, }, ]; } @@ -628,7 +626,7 @@ export default helper(showAll); 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: -```typescript +```typescript {data-filename="app/helpers/join.ts"} import { helper } from '@ember/component/helper'; import { assert } from '@ember/debug'; import { is } from '../../type-utils'; @@ -671,7 +669,7 @@ interface ClassBasedHelper { 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. -```typescript +```typescript {data-filename="app/helpers/greet.ts"} import Helper from '@ember/component/helper'; import { inject as service } from '@ember/service'; import Authentication from 'my-app/services/authentication'; @@ -704,9 +702,7 @@ First, the function we're testing might look like this. 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 [API docs](https://api.emberjs.com/ember/3.14/functions/@ember%2Fdebug/assert)! It’s a development-and-test-only helper that gets stripped from production builds, and is very helpful for this kind of thing! {% endhint %} -```javascript -// app/utils/math.js - +```javascript {data-filename="app/utils/math.js"} export function add(a, b) { assert( 'arguments must be numbers', @@ -719,9 +715,7 @@ export function add(a, b) { Then the test for it might look something like this: -```javascript -// tests/unit/utils/math-test.js - +```javascript {data-filename="tests/unit/utils/math-test.js"} import { module, test } from 'qunit'; import { add } from 'app/utils/math'; @@ -746,9 +740,7 @@ module('the `add` function', function (hooks) { In TypeScript, that wouldn't make any sense at all, because we'd simply add the types to the function declaration: -```typescript -// app/utils/math.ts - +```typescript {data-filename="app/utils/math.ts"} export function add(a: number, b: number): number { assert( 'arguments must be numbers', @@ -761,9 +753,7 @@ export function add(a: number, b: number): number { We might still write tests to make sure what we actually got back was what we expected— -```typescript -// tests/unit/utils/math-test.ts - +```typescript {data-filename="tests/unit/utils/math-test.ts"} import { module, test } from 'qunit'; import { add } from 'app/utils/math'; @@ -778,9 +768,7 @@ module('the `add` function', function (hooks) { —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: -```typescript -// app/utils/math.ts - +```typescript {data-filename="app/utils/math.ts"} export function add(a: number, b: number): number { return a + b; } @@ -794,7 +782,7 @@ Let's return to our silly example with an `add` function. Our setup will look a 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. -```typescript +```typescript {data-filename="app/utils/math.ts"} function add(a: number, b: number): number { assert( 'arguments must be numbers', @@ -807,9 +795,7 @@ function add(a: number, b: number): number { 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! -```javascript -// tests/unit/utils/math-test.js - +```javascript {data-filename="tests/unit/utils/math-test.ts"} import { module, test } from 'qunit'; import { add } from 'app/utils/math'; @@ -847,9 +833,7 @@ We’re going to start by defining a basic `User` and `Profile` so that we have The `User` type is very simple, just an `interface`: -```typescript -// app/types/user.ts - +```typescript {data-filename="app/types/user.ts"} export default interface User { displayName: string; avatarUrl?: string; @@ -858,9 +842,7 @@ export default interface User { Then our component might be defined like this: -```text -{{! app/components/profile.hbs }} - +```handlebars {data-filename="app/components/profile.hbs"}

    - -## Controllers - -Like routes, controllers are just normal 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: - -```typescript {data-filename="app/controllers/heyo.ts"} -import Controller from '@ember/controller'; - -export default class HeyoController extends Controller { - queryParams = [ - { - category: { type: 'array' }, - }, - ]; -} -``` - -Then you will see a type error like this: - -```text -Property 'queryParams' in type 'HeyoController' is not assignable to the same property in base type 'Controller'. - Type '{ category: { type: string; }; }[]' is not assignable to type '(string | Record)[]'. - Type '{ category: { type: string; }; }' is not assignable to type 'string | Record'. - Type '{ category: { type: string; }; }' is not assignable to type 'Record'. - 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: - -```typescript {data-filename="app/controllers/heyo.ts", data-diff="-6,+7"} -import Controller from '@ember/controller'; - -export default class HeyoController extends Controller { - queryParams = [ - { - category: { type: 'array' }, - category: { type: 'array' as const }, - }, - ]; -} -``` - -Now it will type-check. From 728c078c2bbdc07416846504caf4aa9a709462db Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Sun, 1 Oct 2023 11:59:59 -0700 Subject: [PATCH 33/63] Clean up services chapter --- guides/release/using-typescript/core-concepts/index.md | 2 ++ guides/release/using-typescript/core-concepts/services.md | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/guides/release/using-typescript/core-concepts/index.md b/guides/release/using-typescript/core-concepts/index.md index d8b72cb0b1..f321926fad 100644 --- a/guides/release/using-typescript/core-concepts/index.md +++ b/guides/release/using-typescript/core-concepts/index.md @@ -1,3 +1,5 @@ +**Note:** 🚧 This section is under construction! 🏗️ The content here may undergo significant revision in the months ahead! + In the next sections, we will cover how to use TypeScript effectively with specific Ember.js APIs. We do _not_ cover general usage of Ember; instead, we assume that as background knowledge. Please see the [Ember Guides](../..) and [API docs](https://api.emberjs.com)! diff --git a/guides/release/using-typescript/core-concepts/services.md b/guides/release/using-typescript/core-concepts/services.md index c0de32ed58..7d27e65299 100644 --- a/guides/release/using-typescript/core-concepts/services.md +++ b/guides/release/using-typescript/core-concepts/services.md @@ -1,12 +1,12 @@ -## Services +**Note:** 🚧 This section is under construction! 🏗️ The content here may undergo significant revision in the months ahead! -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. +Ember [Services][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 [Ember Guide on Services](../../services/)!) +[services]: ../../../services/ ### A basic service -Let's take this example from the [Ember Guide](../../services/): +Let's take this example from the [Ember Guide](../../services/#toc_defining-services): ```typescript {data-filename="app/services/shopping-cart.ts"} import Service from '@ember/service'; From 3d8ccd97ad7f70c8c08a48b7e41d66269e3c9f8b Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Sun, 1 Oct 2023 12:23:35 -0700 Subject: [PATCH 34/63] Clean up EmberData section --- .../additional-resources/gotchas.md | 1 + .../core-concepts/ember-data.md | 70 +++++++++++-------- 2 files changed, 40 insertions(+), 31 deletions(-) diff --git a/guides/release/using-typescript/additional-resources/gotchas.md b/guides/release/using-typescript/additional-resources/gotchas.md index a227a3432b..410314a8d8 100644 --- a/guides/release/using-typescript/additional-resources/gotchas.md +++ b/guides/release/using-typescript/additional-resources/gotchas.md @@ -93,6 +93,7 @@ function dynamicLookup(owner: Owner) { #### Ember Data lookups + We use the same basic approach for Ember Data type lookups with string keys as we do for service injections, but here we take advantage of the string "type registration" for the runtime code as well. 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. That is, writing `this.store.findRecord('user', 1)` will give you back a `Promise`. diff --git a/guides/release/using-typescript/core-concepts/ember-data.md b/guides/release/using-typescript/core-concepts/ember-data.md index b081bc64e3..56ffac2647 100644 --- a/guides/release/using-typescript/core-concepts/ember-data.md +++ b/guides/release/using-typescript/core-concepts/ember-data.md @@ -1,12 +1,12 @@ **Note:** 🚧 This section is under construction! 🏗️ The content here may not be fully up to date! -In this section, we cover how to use TypeScript effectively with specific Ember Data APIs (anything you'd find under the `@ember-data` package namespace). +In this section, we cover how to use TypeScript effectively with specific EmberData APIs (anything you'd find under the `@ember-data` package namespace). -We do _not_ cover general usage of Ember Data; instead, we assume that as background knowledge. Please see the Ember Data [Guides](https://guides.emberjs.com/release/models) and [API docs](https://api.emberjs.com/ember-data/release)! +We do _not_ cover general usage of EmberData; instead, we assume that as background knowledge. Please see the EmberData [Guides](../../../models) and [API docs](https://api.emberjs.com/ember-data/release)! ## Models -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. +EmberData 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](../ts/decorators.md). @@ -14,13 +14,16 @@ For details about decorator usage, see [our overview of how Ember's decorators w The type returned by the `@attr` decorator is whatever [Transform](https://api.emberjs.com/ember-data/release/classes/Transform) is applied via the invocation. See [our overview of Transforms for more information](./transforms.md). -- 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. +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: @@ -43,9 +46,13 @@ export default class User extends Model { } ``` -**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. +#### Type Safety for Model Attributes + +Even more than with decorators in general, you should be careful when deciding whether to mark a property as [optional `?`][optional] or definitely present (no annotation): EmberData 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 EmberData 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. +[optional]: https://www.typescriptlang.org/docs/handbook/2/objects.html#optional-properties + +The _safest_ type you can write for an EmberData 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: @@ -66,9 +73,9 @@ export default class User extends Model { ### Relationships -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`](https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-cycle.md) error from eslint. +Relationships between models in EmberData 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`](https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-cycle.md) error from eslint. -To avoid these errors, use [type-only imports](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html), available since TypeScript 3.8: +To avoid these errors, use [type-only imports](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html): ```typescript import type User from './user'; @@ -135,13 +142,13 @@ The same basic rules about the safety of these lookups as with `@belongsTo` appl ## Transforms -In Ember Data, `attr` defines an attribute on a [Model](https://guides.emberjs.com/release/models/defining-models/). +In EmberData, `@attr` defines an attribute on a [Model](../../../models/defining-models/). 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`. +EmberData ships with four basic transform types: `string`, `number`, `boolean` and `date`. -You can define your own transforms by subclassing [Transform](https://guides.emberjs.com/release/models/defining-models/#toc_custom-transforms). -Ember Data transforms are normal TypeScript classes. +You can define your own transforms by subclassing [Transform](../../../models/defining-models/#toc_custom-transforms). +EmberData 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: @@ -149,12 +156,6 @@ You may define your own transforms in TypeScript like so: ```typescript {data-filename="app/transforms/coordinate-point.ts"} import Transform from '@ember-data/serializer/transform'; -declare module 'ember-data/types/registries/transform' { - export default interface TransformRegistry { - 'coordinate-point': CoordinatePointTransform; - } -} - export type CoordinatePoint = { x: number; y: number; @@ -170,19 +171,26 @@ export default class CoordinatePointTransform extends Transform { } } -## app/models/cursor.ts -import Model, { attr } from '@ember-data/model'; -import { CoordinatePoint } from 'agwa-data/transforms/coordinate-point'; - declare module 'ember-data/types/registries/transform' { - export default interface ModelRegistry { - cursor: Cursor; + export default interface TransformRegistry { + 'coordinate-point': CoordinatePointTransform; } } +``` + +```typescript {data-filename="app/models/cursor.ts"} +import Model, { attr } from '@ember-data/model'; +import { CoordinatePoint } from 'my-app/transforms/coordinate-point'; export default class Cursor extends Model { @attr('coordinate-point') declare position: CoordinatePoint; } + +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + cursor: Cursor; + } +} ``` -Note that you should declare your own transform under `TransformRegistry` to make `attr` to work with your transform. +Note that you should declare your own transform under `TransformRegistry` to make `@attr` to work with your transform. From 38422ce6bb043de6140723e0c35603e3a17cec41 Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Sun, 1 Oct 2023 15:19:12 -0700 Subject: [PATCH 35/63] Clean up configuration chapter --- guides/release/pages.yml | 4 +- .../additional-resources/gotchas.md | 4 ++ .../application-development/configuration.md | 51 +++++++++++++++---- .../converting-an-app.md | 10 ++++ .../application-development/index.md | 2 +- .../using-typescript/getting-started.md | 8 +-- 6 files changed, 59 insertions(+), 20 deletions(-) diff --git a/guides/release/pages.yml b/guides/release/pages.yml index 2ab977df34..a88ca046d2 100644 --- a/guides/release/pages.yml +++ b/guides/release/pages.yml @@ -272,10 +272,10 @@ - title: 'Application Development' url: 'application-development' pages: - - title: 'Testing' - url: 'testing' - title: 'Configuration' url: 'configuration' + - title: 'Testing' + url: 'testing' - title: 'Converting an Existing Ember App to TypeScript' url: 'converting-an-app' - title: 'Building Addons in TypeScript' diff --git a/guides/release/using-typescript/additional-resources/gotchas.md b/guides/release/using-typescript/additional-resources/gotchas.md index 410314a8d8..6295c156d0 100644 --- a/guides/release/using-typescript/additional-resources/gotchas.md +++ b/guides/release/using-typescript/additional-resources/gotchas.md @@ -321,3 +321,7 @@ export default class MyRoute extends Route { } } ``` + +## ember-cli-typescript + +If you're migrating from `ember-cli-typescript`, particularly an older version, to Ember's out-of-the-box TypeScript support, you may also need to update your `tsconfig.json`. Current versions of `ember-cli-typescript` generate the correct config at installation. You do _not_ need to install `ember-cli-typescript` for new apps or addons. diff --git a/guides/release/using-typescript/application-development/configuration.md b/guides/release/using-typescript/application-development/configuration.md index 9c229fd07d..5924f0bed6 100644 --- a/guides/release/using-typescript/application-development/configuration.md +++ b/guides/release/using-typescript/application-development/configuration.md @@ -1,20 +1,38 @@ **Note:** 🚧 This section is under construction! 🏗️ The content here may not be fully up to date! - - ## `tsconfig.json` -We generate a good default [`tsconfig.json`](https://github.com/typed-ember/ember-cli-typescript/blob/master/blueprint-files/ember-cli-typescript/tsconfig.json), which will usually make everything _Just Work™_. In general, you may customize your TypeScript build process as usual using the `tsconfig.json` file. +If you use the `--typescript` flag when generating your Ember app, we generate a good default [`tsconfig.json`][tsconfig], which will usually make everything _Just Work™_: + +```json {data-filename="tsconfig.json"} +{ + "extends": "@tsconfig/ember/tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "my-app/tests/*": ["tests/*"], + "my-app/*": ["app/*"], + "*": ["types/*"] + } + } +} +``` + +The default `tsconfig.json` extends the [`"@tsconfig/ember/tsconfig.json"`][ember-tsconfig] base, which includes TypeScript compiler options to enable TypeScript development in an Ember app plus some useful default configurations for strictness to ensure type-safety and compatibility with Ember's types. + +Additionally, the generated `tsconfig.json` includes [`baseUrl`][tsconfig-baseUrl] and [`paths`][tsconfig-paths] configuration specific to your app. This configuration allows Ember's classic package layout, which is not resolvable with the Node resolution algorithm, to work with TypeScript. + +In general, you may customize your TypeScript build process as usual using the `tsconfig.json` file. There are a few things worth noting, however, if you're looking to make further or more advanced customizations (but _most_ users can just ignore this section!): + +1. The Ember build pipeline uses Babel's TypeScript support instead of the TypeScript compiler. For this reason, the generated `tsconfig.json` file does not set [`"outDir"`][tsconfig-outDir] and sets [`"noEmit"`][tsconfig-noEmit] to `true`. This configuration allows you to run editors which use the compiler without creating extraneous `.js` files throughout your codebase, leaving the compilation to Babel to manage. -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!): + You _can_ still customize `"outDir"` and `"noEmit"` 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 now generate files at that location. -1. 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. + Note that any changes you _do_ make to `"outDir"` and `"noEmit"` won't have any effect on how _Ember_ builds your application. The build pipeline will continue to use its own temp folder. - 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/[Broccoli](https://broccoli.build) pipeline will continue to use its own temp folder. +1. Since your application is built by Babel, and only _type-checked_ by TypeScript, we set the [`"target"`][tsconfig-target] key in [`"@tsconfig/ember/tsconfig.json"`][ember-tsconfig] 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. -2. 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. -3. 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. -4. 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 [the TypeScript reference materials for `tsconfig.json`](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html). +1. If you make changes to the paths included in or excluded from the build via your `tsconfig.json` (using the [`"include"`][tsconfig-include], [`"exclude"`][tsconfig-exclude], or [`"files"`][tsconfig-files] keys), you will need to restart the server to take the changes into account: the build pipeline does not currently watch the `tsconfig.json` file. ## Enabling Sourcemaps @@ -29,7 +47,7 @@ const app = new EmberApp(defaults, { (Note that this _will_ noticeably slow down your app rebuilds.) -If you are using [Embroider](https://github.com/embroider-build/embroider), you might need to include [devtool](https://webpack.js.org/configuration/devtool/) in your webpack configuration: +If you are using [Embroider], you might need to include [devtool] in your webpack configuration: ```javascript {data-filename="ember-cli-build.js" data-diff="+4"} return require('@embroider/compat').compatBuild(app, Webpack, { @@ -41,4 +59,15 @@ return require('@embroider/compat').compatBuild(app, Webpack, { } ``` -If you're migrating from `ember-cli-typescript`, particularly an older version, to Ember's out-of-the-box TypeScript support, you may also need to update your `tsconfig.json`. Current versions of `ember-cli-typescript` generate the correct config at installation. You do _not_ need to install `ember-cli-typescript` for new apps or adding. +[devtool]: https://webpack.js.org/configuration/devtool/ +[ember-tsconfig]: https://www.npmjs.com/package/@tsconfig/ember +[embroider]: https://github.com/embroider-build/embroider +[tsconfig-baseUrl]: https://www.typescriptlang.org/tsconfig#baseUrl +[tsconfig-exclude]: https://www.typescriptlang.org/tsconfig#exclude +[tsconfig-files]: https://www.typescriptlang.org/tsconfig#files +[tsconfig-include]: https://www.typescriptlang.org/tsconfig#include +[tsconfig-noEmit]: https://www.typescriptlang.org/tsconfig#noEmit +[tsconfig-outDir]: https://www.typescriptlang.org/tsconfig#outDir +[tsconfig-paths]: https://www.typescriptlang.org/tsconfig#paths +[tsconfig-target]: https://www.typescriptlang.org/tsconfig#target +[tsconfig]: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html diff --git a/guides/release/using-typescript/application-development/converting-an-app.md b/guides/release/using-typescript/application-development/converting-an-app.md index e2212f8d70..cd91f4778c 100644 --- a/guides/release/using-typescript/application-development/converting-an-app.md +++ b/guides/release/using-typescript/application-development/converting-an-app.md @@ -2,6 +2,16 @@ +## `ember-cli-build.js` + +To enable TypeScript transpilation in your app, simply add the corresponding configuration for Babel to your `ember-cli-build.js` file. + +```javascript {data-filename="ember-cli-build.js" data-diff="+2"} +const app = new EmberApp(defaults, { + 'ember-cli-babel': { enableTypeScriptTransform: true }, +}); +``` + ### Incremental adoption 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! diff --git a/guides/release/using-typescript/application-development/index.md b/guides/release/using-typescript/application-development/index.md index 455dde7765..ff34bfc576 100644 --- a/guides/release/using-typescript/application-development/index.md +++ b/guides/release/using-typescript/application-development/index.md @@ -1,3 +1,3 @@ --- -redirect: using-typescript/application-development/testing +redirect: using-typescript/application-development/configuration --- diff --git a/guides/release/using-typescript/getting-started.md b/guides/release/using-typescript/getting-started.md index 1b227304a6..be786e3e89 100644 --- a/guides/release/using-typescript/getting-started.md +++ b/guides/release/using-typescript/getting-started.md @@ -12,11 +12,6 @@ ember new my-typescript-app --typescript Using the `--typescript` flag changes the output of `ember new` in a few ways: - - ## TypeScript Project Files Project files will be generated with `.ts` extensions instead of `.js`. @@ -54,7 +49,7 @@ You may be wondering why the `@types` packages are named things like `@types/emb In addition to the usual files added with `ember new`, we also add: -- [`tsconfig.json`](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) +- [`tsconfig.json`](../application-development/configuration/#toc_tsconfig) - `types//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**](https://github.com/typed-ember/ember-cli-typescript/tree/3a434def8b8c8214853cea0762940ccedb2256e8/docs/getting-started/docs/ts/using-ts-effectively/README.md#global-types-for-your-package) for information on its default contents and how to use it effectively @@ -65,3 +60,4 @@ Additionally: - `ember-cli-build.js` will be configured to transform TypeScript at build-time. - `.ember-cli` has `isTypeScriptProject` set to true, which will force the blueprint generators to generate TypeScript rather than JavaScript by default. - `.eslintrc.js` will be configured for TypeScript. +- `lint:types` is added as a script to check types with the command line. From 16c50b71f8dc38dc48990b978d4842976d79a36f Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Mon, 2 Oct 2023 08:58:38 -0700 Subject: [PATCH 36/63] Start editing testing section --- .../application-development/testing.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/guides/release/using-typescript/application-development/testing.md b/guides/release/using-typescript/application-development/testing.md index 86636aea14..9a99a1f531 100644 --- a/guides/release/using-typescript/application-development/testing.md +++ b/guides/release/using-typescript/application-development/testing.md @@ -1,10 +1,10 @@ -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 [Testing Guides](https://guides.emberjs.com/release/testing/) first. The rest of this guide assumes you're already comfortable with testing in Ember! +**Note:** 🚧 This section is under construction! 🏗️ The content here may not be fully up to date! -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. +When working with TypeScript in [Ember tests][testing], there are a few differences in your experience, and there are also differences in how you should handle testing app code vs. addon code. -### App tests +## App tests -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. +One major difference when working with TypeScript in _app_ code is that once your app is _fully_ converted, there are several 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. @@ -84,7 +84,7 @@ export function add(a: number, b: number): number { } ``` -### Addon tests +## Addon tests 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! @@ -133,9 +133,7 @@ module('the `add` function', function(hooks) { }); ``` -### Gotchas - -#### The `TestContext` +## The `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. @@ -309,3 +307,5 @@ If you’ve been around TypeScript a little, and you look up the type of the `Te {% endhint %} 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! + +[testing]: ../../../testing/ From d9ddafc1c7a0d3089ca72d86b35864c3fb97de8b Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Sat, 7 Oct 2023 20:06:58 -0700 Subject: [PATCH 37/63] Incorporate more comments from @chriskrycho --- .../additional-resources/faq.md | 5 +++++ .../core-concepts/invokables.md | 4 +--- .../using-typescript/core-concepts/routing.md | 17 +++++------------ 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/guides/release/using-typescript/additional-resources/faq.md b/guides/release/using-typescript/additional-resources/faq.md index 8d3a9a77f5..f4afdf4d18 100644 --- a/guides/release/using-typescript/additional-resources/faq.md +++ b/guides/release/using-typescript/additional-resources/faq.md @@ -30,6 +30,11 @@ At the root of your application or addon, we include a `types/` direct 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](https://guides.emberjs.com/v2.18.0/configuring-ember/disabling-prototype-extensions/), you can remove these declarations entirely; we include them because they're enabled in most Ember applications today. +We also automatically configure this to support [Glint], which makes 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 `