ember-cli-typescript
Version:
Allow Ember apps to use TypeScript files.
126 lines (84 loc) • 5.99 kB
Markdown
# 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).
## ` `
The type returned by the ` ` 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 ` `, the value is passed through without transformation.
* If you supply one of the built-in transforms, you will get back a corresponding type:
* ` ('string')` → `string`
* ` ('number')` → `number`
* ` ('boolean')` → `boolean`
* ` ('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 '-data/model';
import CustomType from '../transforms/custom-transform';
export default class User extends Model {
()
declare name?: string;
('number')
declare age: number;
('boolean')
declare isAdmin: boolean;
('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 '-data/model';
export default class User extends Model {
()
declare name?: string;
('number', { defaultValue: 13 })
declare age: number;
('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';
```
### ` `
The type returned by the ` ` decorator depends on whether the relationship is `{ async: true }` \(which it is by default\).
* If the value is `true`, the type you should use is `AsyncBelongsTo<Model>`, where `Model` is the type of the model you are creating a relationship to.
* If the value is `false`, the type is `Model`, where `Model` is the type of the model you are creating a relationship to.
So, for example, you might define a class like this:
```typescript
import Model, { belongsTo, type AsyncBelongsTo } from '-data/model';
import type User from './user';
import type Site from './site';
export default class Post extends Model {
('user')
declare user: AsyncBelongsTo<User>;
('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<Model>` object, which itself may or may not ultimately resolve to a value—depending on the API response—but will always be present itself.
* accessing a non-async relationship which is known to be associated but has not been loaded will trigger an error, so all access to the property will be safe _if_ it resolves at all.
Note, however, that this type-safety is not a guarantee of there being no runtime error: you still need to uphold the contract for non-async relationships \(that is: loading the data first, or side-loading it with the request\) to avoid throwing an error!
### ` `
The type returned by the ` ` decorator depends on whether the relationship is `{ async: true }` \(which it is by default\).
* If the value is `true`, the type you should use is `AsyncHasMany<Model>`, where `Model` is the type of the model you are creating a relationship to.
* If the value is `false`, the type is `SyncHasMany<Model>`, where `Model` is the type of the model you are creating a relationship to.
So, for example, you might define a class like this:
```typescript
import Model, { hasMany, type AsyncHasMany, type SyncHasMany } from '-data/model';
import type Comment from './comment';
import type User from './user';
export default class Thread extends Model {
('comment')
declare comments: AsyncHasMany<Comment>;
('user', { async: false })
declare participants: SyncHasMany<User>;
}
```
The same basic rules about the safety of these lookups as with ` ` apply to these types. The difference is just that in ` ` the resulting types are _arrays_ rather than single objects.