# mobx-keystone
# intro
`mobx-keystone` helps you build complex client-side apps with a single source of truth, mutable model code, and immutable traceability built in. You write straightforward actions and computed values, while the library gives you snapshots, patches, undo/redo, and runtime protection on top.
You can think of it as a TypeScript-first model layer on top of MobX that scales better as your domain grows.
- Start here: [Installation](./installation.mdx)
- Learn by building: [Class Models](./classModels.mdx)
- See a full example: [Todo List Example](./examples/todoList/todoList.mdx)
- Migrating from MST: [Migration Guide](./mstMigrationGuide.mdx)
- API reference: [API docs](/api/)
[](https://www.npmjs.com/package/mobx-keystone)


[](https://github.com/xaviergonz/mobx-keystone/actions/workflows/main.yml)
[](https://codecov.io/gh/xaviergonz/mobx-keystone)
[](https://app.netlify.com/sites/mobx-keystone/deploys)
## Quick glance
```ts
@model("todo/Todo")
class Todo extends Model({
text: prop(""),
done: prop(false),
}) {
@modelAction
toggle() {
this.done = !this.done
}
}
@model("todo/Store")
class TodoStore extends Model({
todos: prop(() => []),
}) {
@computed
get pendingCount() {
return this.todos.filter((t) => !t.done).length
}
@modelAction
addTodo(text: string) {
this.todos.push(new Todo({ text }))
}
}
const store = new TodoStore({})
registerRootStore(store)
```
## Why teams choose mobx-keystone
- Mutable action code with protected updates, so state changes stay explicit and safe.
- Runtime snapshots and JSON patches for persistence, sync, replay, and debugging.
- Built-in primitives for references, transactions, action middlewares, and undo/redo.
- Strong TypeScript inference for models, snapshots, and actions.
- Composable domain models that stay maintainable as app complexity grows.
- Seamless integration with MobX and `mobx-react`.
## What users are saying
> "I've never been so in love with a tool. [...] In my eyes it is the perfect state management tool for TS/React projects. [...] Building complex clientside apps has never been so easy and fun for me."
>
> - [@finallyblueskies](https://github.com/finallyblueskies), [#538](https://github.com/xaviergonz/mobx-keystone/issues/538)
> "I'm absolutely loving this project. [...] It's taken all the best bits from mobx and mobx-state-tree and put them into a single package that's a joy to work with. [...] You've literally thought of everything!"
>
> - [@robclouth](https://github.com/robclouth), [#146](https://github.com/xaviergonz/mobx-keystone/issues/146)
> "Thank you for this amazing library. [...] After evaluating every state library out there, mobx-keystone stands out [...] a true single source of truth for records, first-class relationships [...] first-class reactivity."
>
> - [@lolmaus](https://github.com/lolmaus), [#566](https://github.com/xaviergonz/mobx-keystone/issues/566)
> "First of all thank you for this amazing library, it really is a joy to work with."
>
> - [@exception-producer](https://github.com/exception-producer), [#559](https://github.com/xaviergonz/mobx-keystone/issues/559)
> "mobx-keystone is awesome and way easier to use for typesafe domain modeling than MST."
>
> - [@krnsk0](https://github.com/krnsk0), [#467](https://github.com/xaviergonz/mobx-keystone/issues/467)
## How it works
At the center of `mobx-keystone` is a _living tree_ of mutable but strictly protected models, arrays, and plain objects. You update state through model actions, and immutable structurally shared snapshots are derived automatically.
This gives you mutability where it helps developer experience, plus immutable traceability where it helps reliability.
Trees can only be modified by actions that belong to the same subtree. Actions are replayable and can be distributed, and fine-grained changes can be observed as JSON patches.
Because `mobx-keystone` uses MobX behind the scenes, it integrates naturally with [`mobx`](https://mobx.js.org) and [`mobx-react`](https://github.com/mobxjs/mobx-react). The snapshot and middleware system also makes it possible to replace a Redux reducer/store pair with model-driven state and connect Redux devtools.
`mobx-keystone` consists of composable _models_ that capture domain state and behavior together. Model instances are created from props, protect their own updates, and reconcile efficiently when applying snapshots.
## Resources for LLM workflows
Need markdown exports for AI/LLM tools? See [llms.txt](/llms.txt), [llms-full.txt](/llms-full.txt), or the MD docs intro.
---
# installation
This library requires a more or less modern JavaScript environment to work, namely one with support for:
- MobX 6, 5, or 4 (with its gotchas)
- Proxies
- Symbols
- WeakMap/WeakSet
In other words, it should work on mostly anything except _it won't work in Internet Explorer_.
If you are using TypeScript:
- Version 5.0+ is recommended (standard decorators).
- Legacy decorators are also supported (`experimentalDecorators: true`).
## Transpiler configuration
This library uses JavaScript decorators and class properties.
### Standard decorators
Use this mode if you are on TypeScript 5+ and want standard decorators.
For Babel, ensure class static blocks are transformed (for example via `@babel/preset-env` targets, or by adding `@babel/plugin-transform-class-static-block`).
### Legacy decorators
Use this mode if your project is on legacy decorators (`experimentalDecorators: true`).
---
# mstComparison
This library is very much like `mobx-state-tree` and takes lots of ideas from it, so the transition
should be fairly simple. There are some trade-offs though, as shown in the following chart:
| Feature | `mobx-keystone` | `mobx-state-tree` |
| ----------------------------------------- | --------------------------------------------- | --------------------------- |
| Tree-like structure | | |
| Immutable snapshot generation | | |
| Patch generation | | |
| Action serialization / replaying | | |
| Action middleware support | (1) | |
| - Atomic/Transaction middleware | | |
| - Undo manager middleware | | |
| Flow action support | | |
| References | | |
| Frozen data | | |
| TypeScript support | (2) | |
| Simplified instance / snapshot type usage | | |
| Simplified model life-cycle | | |
| Runtime type validation | (3) | |
| No metadata inside snapshots | (4) | |
| Redux compatibility layer | | |
1. Includes an improved action tracking middleware that makes it easier to create
middlewares for flow (async) actions.
2. Support for self-model references / cross-model references / no need for late types, no need for casting,
etc.
3. Runtime type checking / type definitions are completely optional in `mobx-keystone`.
4. Only when using data models, although they lack life-cycle support.
## TypeScript improvements
`mobx-state-tree` has some limitations when it comes to TypeScript typings, which `mobx-keystone` tries to overcome.
### If you know TypeScript you already know how to type models
When not using runtime type checking, `mobx-keystone` uses standard TypeScript type annotations to declare model data, which lowers the learning curve.
However, if you need runtime type checking, `mobx-keystone` includes a completely optional type definition / runtime type checking system as well.
### Self-recursive and cross-referenced models
Self-recursive or cross-referenced models are impossible (or at least very hard) to properly type in `mobx-state-tree`, but they become trivial with `mobx-keystone`.
```ts
// self recursive model
@model("myApp/TreeNode")
class TreeNode extends Model({ children: prop(() => []) }) {}
// cross-referenced models
@model("myApp/A")
class A extends Model({ b: prop() }) {}
@model("myApp/B")
class B extends Model({ a: prop() }) {}
```
### Simpler instance / snapshot type usage
Another area of improvement is the simplification of the usage of snapshot vs. instance types. In `mobx-state-tree` it is possible to assign snapshots to properties, as well as actual instances, but the actual type of properties are instances, which leads to confusing casts and constructs such as:
```ts
// mobx-state-tree code
const Todo = types
.model({
done: false,
text: types.string,
})
.actions((self) => ({
setText(text: string) {
self.text = text
},
setDone(done: boolean) {
self.done = done
},
}))
const RootStore = types
.model({
selected: types.maybe(Todo),
})
.actions((self) => ({
// note the usage of a union of the snapshot type and the instance type
setSelected(todo: SnapshotIn | Instance) {
// note the usage of cast to indicate that it is ok to use a snapshot when
// the property actually expects an instance
self.selected = cast(todo)
},
}))
```
In `mobx-keystone` snapshots are usually only expected when dealing with `getSnapshot` and `fromSnapshot`, so it leads to a simpler usage:
```ts
@model("myApp/Todo")
class Todo extends Model({
done: prop(false).withSetter(),
text: prop().withSetter(),
}) {}
@model("myApp/RootStore")
class RootStore extends Model({
selected: prop(undefined).withSetter(),
}) {}
```
### Less confusion between this/self usages - use of standard computed decorators
Usually in `mobx-state-tree` code from a previous "chunk" (actions, views) has to be accessed using `self`, while code in the same "chunk" has to be accessed using `this` to get proper typings:
```ts
// mobx-state-tree code
const Todo = types
.model({
done: false,
text: types.string,
title: types.string,
})
.views((self) => ({
get asStr() {
// here we use `self` since the properties come from a previous chunk
return `${self.text} is done? ${self.done}`
},
get asStrWithTitle() {
// here we use `this` for `asStr` since it comes from the current chunk
return `${self.title} - ${this.asStr}`
},
}))
```
In `mobx-keystone` `this` can always be used, plus the standard `computed` MobX decorator (including extra options):
```ts
@model("myApp/Todo")
class Todo extends Model({
done: prop(false),
text: prop(),
title: prop(),
}) {
@computed
get asStr() {
return `${this.text} is done? ${this.done}`
}
@computed
get asStrWithTitle() {
return `${this.title} - ${this.asStr}`
}
}
```
## Simplified model life-cycle
`mobx-state-tree` has a couple of life-cycle hooks (`afterCreate`, `afterAttach`, `beforeDetach`, `beforeCreate`) that might or might not trigger when you think they should due to the lazy initialization of nodes.
For example, you might create a submodel with an `afterCreate` hook, but it might never be actually executed unless the node contents are accessed (due to lazy initialization).
Maybe you might want to set up an effect (`reaction` or the like), but you only want that effect to work after it actually becomes part of your application state.
Likewise, you might want to call `getRoot` to access the root model, but it might actually not give the value you expect until the model is attached to a parent which is eventually (or not) attached to the proper root.
`mobx-keystone` solves this by only offering two life-cycle hooks:
1. `onInit` which is _always_ called once the model has been created (and since there's no lazy initialization they will always be)
1. `onAttachedToRootStore` (plus an optional disposer that gets executed when it is detached) which gets called once the model gets attached to the proper root node (a root store), thus ensuring that at that point `getRoot` will return the expected value and makes it a perfect place to set up effects (more info in the [class models](./classModels.mdx) section)
---
# classModels
## Overview
`mobx-keystone` supports the following kinds of data:
- Class models, which are like objects but enhanced with local behaviors (actions/views) and life-cycle events (hooks).
- Data models, which only define behaviors (actions/views) over "untainted" data and have a _very_ limited number of life-cycle events (hooks).
- Objects, which serve as basic storages of data (kind of like class models, except without actions and life-cycle events), as well as key-value maps of other data.
- Arrays.
- Primitive values (`string`, `boolean`, `number`, `null`, `undefined`).
In this section we will focus on class models, since the other types can be used as children in the usual way.
## Your first class model
A class model for a todo can be defined as follows:
```ts
// the model decorator marks this class as a model, an object with actions, etc.
// the string identifies this model type and must be unique across your whole application
@model("myCoolApp/Todo")
export class Todo extends Model({
// here we define the type of the model data, which is observable and snapshotable
// and also part of the required initialization data of the model
// in this case we don't use runtime type checking
text: prop(), // a required string
done: prop(false), // an optional boolean that will default to `false` when the input is `null` or `undefined`
// if you want to make a property truly optional then use `x: prop()`
// if we required runtime type checking we could do this
// text: tProp(types.string),
// done: tProp(types.boolean, false),
// if you want to make a property truly optional then use `x: tProp(types.maybe(TYPE))`
}) {
// the `modelAction` decorator marks the method as a model action, giving it access
// to modify any model data and other superpowers such as action
// middlewares, replication, etc.
@modelAction
setDone(done: boolean) {
this.done = done
}
@modelAction
setText(text: string) {
this.text = text
}
@computed
get asString() {
return `${!this.done ? "TODO" : "DONE"} ${this.text}`
}
}
```
Note that there are several ways to define properties.
Without runtime type checking:
- `prop(options?: ModelOptions)` - A property of a given type, with no default set if it is `null` or `undefined` in the initial data.
- `prop(defaultValue: T, options?: ModelOptions)` - A property of a given type, with a default set if it is `null` or `undefined` in the initial data. Use this only for default primitives.
- `prop(defaultFn: () => T, options?: ModelOptions)` - A property of a given type, with a default value generator if it is `null` or `undefined` in the initial data. Usually used for default objects / arrays / models.
With runtime type checking (check the relevant section for more info):
- `tProp(type, options?: ModelOptions)` - A property of a given runtime checked type, with no default set if it is `null` or `undefined` in the initial data.
- `tProp(type, defaultValue: T, options?: ModelOptions)` - A property of a given runtime checked type, with a default set if it is `null` or `undefined` in the initial data. Use this only for default primitives.
- `tProp(type, defaultFn: () => T, options?: ModelOptions)` - A property of a given runtime checked type, with a default value generator if it is `null` or `undefined` in the initial data. Usually used for default objects / arrays / models.
## Class model rules
The rules that need to be followed to declare a class model are:
- Class models have to be decorated with `@model` and require a unique across-application ID for the model type.
- They have to extend `Model`, which in TypeScript requires the type of the data that will become observable / snapshotable / patchable.
- This data (that is observable and part of the snapshot) can be accessed / changed through `this` as well as `this.$`.
- Model actions need to be used in order to be able to change such data.
- Never declare your own constructor, there are life-cycle events for that (more on that later).
Of course primitives are not the only kinds of data that a class model can hold. Arrays, plain objects and other objects can be used as well.
## Creating a class model instance
An instance of the todo model above can be created like this:
```ts
const myTodo1 = new Todo({ done: true, text: "buy some milk" })
// note how `done` can be skipped since it was declared with a default value
const myTodo2 = new Todo({ text: "buy some coffee" })
```
## Automatic class model actions for property setters
Most times, the only action we need for a property is a setter. We can use the prop modifier `withSetter()` (`withSetter("assign")` has been deprecated) to reduce boilerplate and generate property setters. For example, the model above could be written as:
```ts
@model("myCoolApp/Todo")
export class Todo extends Model({
text: prop().withSetter(),
done: prop(false).withSetter(),
}) {}
const myTodo = new Todo({ text: "buy some coffee" })
// this is now allowed and properly wrapped in two respective actions
myTodo.setText("buy some milk")
myTodo.setDone(true)
```
If for some reason you still require to change these without using a `modelAction`, consider using `objectActions.set`, `objectActions.delete`, `objectActions.call`, `arrayActions.push`, etc. if needed.
`withSetter` also accepts a value transform function. For example `withSetter(cloneTreeValue)` clones incoming non-primitive values before setting them, which is useful when the same object instance may be reused across different places in the tree. If you need clone options, wrap it (for example `withSetter((x) => cloneTreeValue(x, { generateNewIds: false }))`).
Note that the transform function is only applied when using the generated setter method (for example, `model.setX(3)`); it is **not** applied during model construction (`new Model({ x: 3 })`) or snapshot deserialization, where the value is assigned as provided.
## Life-cycle event hooks
Class models can optionally include an implementation for each of the life-cycle hooks:
- `onInit()`, which serves as a replacement for the constructor and will fire as soon as the model is created.
On most occasions, it is better to use the next hook.
- `onAttachedToRootStore(rootStore)`, which fires once a class model becomes part of a root store tree and which can optionally return a disposer which will run once the model detaches from such root store tree.
It will be explained in detail in the [root stores](./rootStores.mdx) section.
## Runtime data
Runtime data (data that doesn't need to be snapshotable, or that needs to be tracked in any way) can be declared as a usual property. Nothing special is needed.
```ts
@model("myApp/SomeModel")
class SomeModel extends Model({
title: prop("demo"),
}) {
// non-observable runtime data
saveCount = 0
markSaved() {
this.saveCount++
}
// or observable in the usual MobX way
@observable
isSaving = false
@action
setSaving(isSaving: boolean) {
this.isSaving = isSaving
}
}
```
## Accessing the type and ID of a class model
It is interesting to observe that class models include a property named `$modelType`:
```ts
myTodo1.$modelType // "myCoolApp/Todo"
```
This property ends up in the snapshot representation of the model and allows `fromSnapshot` to reconstruct the correct model class, so it is usually required.
That being said, there are actually two ways to skip this requirement (having `$modelType` in input snapshots):
- If a property is typed using `tProp` then the model(s) in that property won't need `$modelType`.
- If a model is constructed using the `fromSnapshot` overload that takes a type as first parameter then that snapshot won't need `$modelType`.
### Runtime model registration (`registerModels`)
Most applications do not need to do anything special: importing and using model classes normally is enough for `@model(...)` decorators to register them.
You may need manual registration when your app mainly deserializes snapshots and some model imports are only used as TypeScript types (or otherwise elided by your build pipeline). In that case, `fromSnapshot` may fail with:
```txt
model with name "..." not found in the registry
```
To make registration explicit, call `registerModels(...)` once at app startup with runtime class references:
```ts
registerModels(RootStore, User)
```
This does not instantiate models; it only verifies the classes are loaded and registered.
## Setting an ID property
Note that it is also possible to assign a property as the ID property using `idProp`. Setting a dedicated ID property has some advantages:
- Improved reconciliation when applying snapshots.
- Resolving the target of serialized actions will be less likely to hit the wrong node (thanks to ID checking).
- Root references will be able to resolve the nodes without any extra configuration.
```ts
@model("myApp/ModelWithCustomId")
class ModelWithCustomId extends Model({
customId: idProp
}) {
...
}
```
In this case either `customId` (both in snapshots and instances) or `$modelId` (only in instances) can be used to read / write the ID property.
Just make sure that ID is unique for every model object (no matter its type).
Note that when using the `new` operator to create a new instance you can either not specify the property so it will be auto-generated, or you can specify it directly:
```ts
const myTodo = new Todo({
customId: "my custom id",
})
```
as well as writing to it at a later time inside a model action:
```ts
// inside some model action
this.myId = "my new custom id"
// or alternatively
this.$modelId = "my new custom id"
```
If you wish to type the ID property even further (for example by using a string template) you may do it like this:
```ts
idProp.typedAs<`custom-${string}`>
```
## Customizing the ID generator function
The default model ID generator function is tuned up to be fast and works like this:
```ts
const baseLocalId = nanoid()
let localId = 0
function generateModelId() {
return localId.toString(36) + "-" + baseLocalId
}
```
This has the implications however that every model ID generated by the same client / server session will have a different first part of the ID, yet share the same last part of such ID.
That being said, it is possible to use a custom function to generate model IDs using `setGlobalConfig`:
```ts
setGlobalConfig({
modelIdGenerator: myModelIdGeneratorFunction,
})
```
You can also define a generator per model ID field using `idProp.withGenerator(...)`:
```ts
@model("myApp/Todo")
class Todo extends Model({
id: idProp.withGenerator(() => `todo-${uuid()}`),
text: prop(),
}) {}
```
For `ExtendedModel`, the generator follows inheritance:
- If base has one and extended does not override the id prop, extended uses base generator.
- If extended overrides a base id prop, `withGenerator(...)` must match the base one exactly (including `undefined`).
- If base has no id prop and extended defines one, extended uses its own generator.
## Getting the TypeScript types for model data and model creation data
- `ModelData` is the type of the model props without transformations (as accessible via `model.$`).
- `ModelCreationData` is the type of the first parameter passed to `new Model(...)`.
For example, given:
```ts
@model("myCoolApp/Todo")
export class Todo extends Model({
text: prop(), // a required string
done: prop(false), // an optional boolean that will default to `false`
}) {}
```
`ModelCreationData` would be:
```ts
{
text: string; // required when passing it to `new Todo({...})`
done?: boolean | null; // optional when passing it to `new Todo({...})`
}
```
and `ModelData` would be:
```ts
{
text: string // since it will always be present when accessing `todo.text`
done: boolean // since it will always be present when accessing `todo.done`
}
```
## Flows (async actions)
While `@modelAction` defines sync model actions, async model actions are possible as well with the use of `@modelFlow`:
```ts
interface Book {
title: string
price: number
}
@model("myApp/BookStore")
class BookStore extends Model({
books: prop(() => []),
}) {
// TypeScript version
@modelFlow
// note: `_async` is a function that has to be imported, we have to use `this: THISCLASS`
fetchMyBooksAsync = _async(function* (this: BookStore, token: string) {
// we use `yield* _await(X)` where we would use `await X`
// note: it is `yield*`, NOT just `yield`; `_await` is a function that has to be imported
const myBooks = yield* _await(myBackendClient.getBooks(token))
this.books = myBooks
});
// JavaScript version
@modelFlow
// we use `function*` (a function generator) where we would use `async`
*fetchMyBooksAsync(token) {
// we use `yield* _await(X)` where we would use `await X`
// note: it is `yield*`, NOT just `yield`; `_await` is a function that has to be imported
const myBooks = yield* _await(myBackendClient.getBooks(token))
this.books = myBooks
}
}
// in either case it can be used like this
const myBookStore = new BookStore({})
await myBookStore.fetchMyBooksAsync("someToken")
```
## Value-type class models
Sometimes it is useful to have models that act like a primitive. This is, models that would get automatically cloned when attached to a tree rather than relying on having a single instance of it in the whole tree.
For example, say that you have a model for a RGB color:
```ts
@model("myApp/Color")
class Color extends Model(
{
r: prop(),
g: prop(),
b: prop(),
},
{
valueType: true,
}
) {}
```
Usually, without `valueType: true`, whenever we wanted to use the same color in two separate paths of the tree we would need to clone it first, or else we would get an error about the node trying to have two parents at once. With `valueType` set this would be no longer the case, since the node would get cloned automatically if it already had a parent:
```ts
class MyColors extends Model({
primary: prop(),
secondary: prop(),
}) {
// ...
}
// without `valueType: true` this would throw an error
// but with it `primary` is now a clone of `secondary`
myColors.setPrimary(myColors.secondary)
```
Note that it is an actual clone, meaning changing the primary color won't change the secondary one.
## Factory pattern / Generics
If you are _not_ relying on `tProp` to do runtime type checking it is possible to use this pattern to get generic classes:
```ts
@model("myApp/GenericPoint")
class GenericPoint extends Model(() => ({
x: prop(),
y: prop(),
})) {
@modelAction
setXY(x: T, y: T) {
this.x = x
this.y = y
}
}
@model("myApp/Generic3dPoint")
class Generic3dPoint extends ExtendedModel(() => ({
baseModel: modelClass>(GenericPoint),
props: {
z: prop(),
},
})) {
// ...
}
```
If you rely on `tProp` (and also `prop` really) a different possibility is to use a factory pattern with class models. For example:
```ts
function createModelClass(modelName: string, initialX: TX, initialY: TY) {
@model(`myApp/${modelName}`)
class MyModel extends Model({
x: prop(() => initialX),
y: prop(() => initialY),
}) {
@modelAction
setXY(x: TX, y: TY) {
this.x = x
this.y = y
}
}
return MyModel
}
const NumberMyModel = createModelClass("NumberMyModel", 10, 20)
type NumberMyModel = InstanceType
const numberMyModelInstance = new NumberMyModel({}) // this will be of type `NumberMyModel`
numberMyModelInstance.setXY(50, 60)
const StringMyModel = createModelClass("StringMyModel", "10", "20")
type StringMyModel = InstanceType
const stringMyModelInstance = new StringMyModel({}) // this will be of type `StringMyModel`
stringMyModelInstance.setXY("50", "60")
```
Note that the above will only work when not generating declaration maps. If you need to generate declarations (for example for a library) then it is a bit more tedious, but still possible:
```ts
export function createModelClass(modelName: string, initialX: TX, initialY: TY) {
const MyModelProps = Model({
x: prop(() => initialX),
y: prop(() => initialY),
})
@model(`myApp/${modelName}`)
class MyModel extends MyModelProps {
@modelAction
setXY(x: TX, y: TY) {
this.x = x
this.y = y
}
}
return MyModel as ModelClassDeclaration<
typeof MyModelProps,
{
setXY(x: TX, y: TY): void
}
>
}
```
## Inheritance
Model inheritance is possible with a few gotchas.
The first thing to bear in mind is that class models that extend from other class models must use `ExtendedModel` rather than the plain `Model`. For example:
```ts
@model("MyApp/Point")
class Point extends Model({
x: prop(),
y: prop(),
}) {
get sum() {
return this.x + this.y
}
}
// note how `ExtendedModel` is used
@model("MyApp/Point3d")
class Point3d extends ExtendedModel(Point, {
z: prop(),
}) {
get sum() {
return super.sum + this.z
}
}
```
Also, remember that if your base model has `onInit` / `onAttachedToRootStore` and you redeclare them in your extended model you will need to call `super.onInit(...)` / `super.onAttachedToRootStore(...)` in the extended model.
If you want to extend a generic class, then you may want to use `modelClass` in order to specify the exact generic like this:
```ts
class X extends ExtendedModel(modelClass>(SomeGenericClass), { ... }) { ... }
```
If you don't it will still compile, but the generic will be assumed to have `unknown` for all its generic parameters.
## Mixins
If you want a constrained mixin-style API for class models you can use `defineModelMixin` and `composeMixins`.
This avoids repeating `as unknown as ModelClass<...>` in each factory.
Pass the model properties as the first argument; an optional builder as the last argument adds methods.
Use `req()` as the second argument to declare that the base model must already expose a given shape before the mixin is applied.
```ts
@model("myApp/Entity")
class Entity extends Model({}) {}
// Props only – no builder needed
const countableMixin = defineModelMixin(
{ quantity: tProp(types.number, 0) }
)
// Props + methods via builder
const countableWithMethodsMixin = defineModelMixin(
{ quantity: tProp(types.number, 0) },
(Base) =>
class Countable extends Base {
incrementBy(delta: number) {
return this.quantity + delta
}
}
)
// Constrained mixin – requires `quantity` to already be on the base
const producerMixin = defineModelMixin(
{ produced: tProp(types.number, 0) },
req<{ quantity: number }>(),
(Base) =>
class Producer extends Base {
produceTotal() {
return this.produced + this.quantity
}
}
)
const ProductBase = composeMixins(Entity, countableWithMethodsMixin, producerMixin)
type Product = InstanceType
```
`ModelData` and `ModelCreationData` reflect the exact prop types from all composed mixins.
`composeMixins` enforces requirements at the type level: applying `producerMixin` before `countableWithMethodsMixin` is a TypeScript error.
When you need to derive a constraint from an already-composed base, extract the instance type:
```ts
const CountableBase = composeMixins(Entity, countableWithMethodsMixin)
type Countable = InstanceType
```
You can also call `composeMixins` **without a base class** when you want to bundle mixins for reuse without any shared base.
An implicit empty base is used, so you can apply the bundle to multiple concrete classes:
```ts
// Bundle mixins independently of any base
const ProductBundle = composeMixins(countableWithMethodsMixin, producerMixin)
@model("myApp/Product")
class Product extends ExtendedModel(ProductBundle, {}) {}
@model("myApp/Order")
class Order extends ExtendedModel(ProductBundle, {}) {}
```
You can also inline `composeMixins` directly into `ExtendedModel` when you don't need the composed base as a separate variable:
```ts
@model("myApp/Product")
class Product extends ExtendedModel(
composeMixins(Entity, countableWithMethodsMixin, producerMixin),
{ /* any additional own props */ }
) {}
```
## Snapshot pre/post-processors: `fromSnapshotProcessor` / `toSnapshotProcessor`
`fromSnapshotProcessor` might be used to transform an input snapshot into the model's expected input snapshot.
This is useful, for example, for versioning.
Note that the `$modelType` property will always be there and will remain unchanged no matter the transformation.
```ts
// we split it here so it is accessible to `FromSnapshotDefaultType<>`
const modelProps = {
// in version 2 we split `fullName` into `firstName` and `lastName`
_version: prop(2),
firstName: prop(),
lastName: prop(),
}
@model("name")
class Name extends Model(modelProps, {
fromSnapshotProcessor(
sn: FromSnapshotDefaultType | { _version: 1; fullName: string }
) {
if (sn._version === 2) {
return sn
}
const [firstName, lastName] = sn.fullName.split(" ")
return {
_version: 2,
firstName,
lastName,
}
},
}) {
// ...
}
```
`toSnapshotProcessor` is the opposite and might be used to transform the model's expected output snapshot into another kind of object snapshot.
Note that the `$modelType` property will always be there and will remain unchanged no matter the transformation.
```ts
@model("name")
class Name extends Model(
{
firstName: prop(),
lastName: prop(),
},
{
toSnapshotProcessor(sn, modelInstance) {
// we want to also keep `fullName` for backwards compatibility
return {
...sn,
fullName: `${sn.firstName} ${sn.lastName}`,
}
},
}
) {
// ...
}
```
## Snapshot pre/post-processors for model properties
Model properties can have their own snapshot processors as well:
In this example we use snapshot processors to serialize a string array property as a comma-separated string.
```ts
class M extends Model({
names:
prop(() => [])
.withSnapshotProcessor({
fromSnapshot: (sn: string) => sn.split(",")
toSnapshot: (sn) => sn.join(",")
})
})
```
## Usage without decorators
Although this library was primarily intented to be used with decorators it is also possible to use it without them.
To do so you can use the `decoratedModel` function as shown below:
```ts
// note the `_` at the beginning of the name to distinguish it from the decorated version
class _Todo extends Model({
text: prop(),
done: prop(false),
}) {
// note how here we don't decorate the method directly, but on the next parameter instead
// @modelAction
setDone(done: boolean) {
this.done = done
}
// @modelAction
setText(text: string) {
this.text = text
}
// @computed
get fullText() {
return `${this.done ? "DONE" : "TODO"} - ${this.text}`
}
}
const Todo = decoratedModel(
// the string identifies this model type and must be unique across your whole application
// you may pass `undefined` if you don't want the model to be registered yet (e.g. for a base class)
"myCoolApp/Todo",
_Todo,
,
// here we pass what we would use as decorators to the class methods/properties above
// if we want to use multiple chained decorators we can pass an array of them instead
// note that any kind of TypeScript-compatible decorator is supported, not only the built-in ones!
{
setDone: modelAction,
setText: modelAction,
fullText: computed,
}
)
// needed to be able to do `SnapshotInOf`, type a variable as `Todo`, etc.
type Todo = _Todo
// if `_Todo` was generic then it would be `type Todo = _Todo`
const myTodo = new Todo({ done: false, text: "buy some milk" })
```
---
# dataModels
## Overview
Data models, like class models, define the behaviors (actions/views) that can be performed over data, but without tainting the data itself with `$modelType`. This comes with some disadvantages as well:
- The model instances are created lazily and when needed rather than eagerly.
- The only life-cycle event hook available, `onLazyInit`, runs lazily, meaning the first time the data model wrapper is created rather than eagerly.
- Reconciliation is somewhat worse due to the lack of an ID property to uniquely identify the instances.
That being said, they have some use cases (for example to represent a backend response that does not include `$modelType` yet needs to be modified locally and eventually sent back).
## Your first data model
Data models are defined in a similar way to class models, except that they use `DataModel` instead of `Model`. One thing to note though is that default values for properties are only applied when using `new` over plain objects (i.e. not tree nodes):
```ts
// the model decorator marks this class as a model, an object with actions, etc.
// the string identifies this model type and must be unique across your whole application
@model("myCoolApp/Todo")
export class Todo extends DataModel({
// here we define the type of the model data, which is observable and snapshotable
// and also part of the required initialization data of the model
// in this case we don't use runtime type checking
text: prop(), // a required string
done: prop(false), // an optional boolean that will default to `false` when the input is `null` or `undefined`
// if you want to make a property truly optional then use `x: prop()`
// if we required runtime type checking we could do this
// text: tProp(types.string),
// done: tProp(types.boolean, false),
// if you want to make an optional property then use `x: tProp(types.maybe(TYPE))`
}) {
// the `modelAction` decorator marks the method as a model action, giving it access
// to modify any model data and other superpowers such as action
// middlewares, replication, etc.
@modelAction
setDone(done: boolean) {
this.done = done
}
@modelAction
setText(text: string) {
this.text = text
}
@computed
get asString() {
return `${!this.done ? "TODO" : "DONE"} ${this.text}`
}
}
```
Note that there are several ways to define properties.
Without runtime type checking:
- `prop(options?: ModelOptions)` - A property of a given type, with no default set if it is `null` or `undefined` in the initial data passed to `new`.
- `prop(defaultValue: T, options?: ModelOptions)` - A property of a given type, with a default set if it is `null` or `undefined` in the initial data passed to `new`. Use this only for default primitives.
- `prop(defaultFn: () => T, options?: ModelOptions)` - A property of a given type, with a default value generator if it is `null` or `undefined` in the initial data passed to `new`. Usually used for default objects / arrays / models.
With runtime type checking (check the relevant section for more info):
- `tProp(type, options?: ModelOptions)` - A property of a given runtime checked type, with no default set if it is `null` or `undefined` in the initial data passed to `new`.
- `tProp(type, defaultValue: T, options?: ModelOptions)` - A property of a given runtime checked type, with a default set if it is `null` or `undefined` in the initial data passed to `new`. Use this only for default primitives.
- `tProp(type, defaultFn: () => T, options?: ModelOptions)` - A property of a given runtime checked type, with a default value generator if it is `null` or `undefined` in the initial data passed to `new`. Usually used for default objects / arrays / models.
## Data model rules
The rules that need to be followed to declare a data model are:
- Data models have to be decorated with `@model` and require a unique across-application ID for the model type.
- They have to extend `DataModel`, which in TypeScript requires the type of the data that will become observable / snapshotable / patchable.
- This data (that is observable and part of the snapshot) can be accessed / changed through `this` as well as `this.$`.
- Model actions need to be used in order to be able to change such data.
- Never declare your own constructor, there are life-cycle events for that (more on that later).
Of course primitives are not the only kinds of data that a data model can hold. Arrays, plain objects, and other objects can be used as well.
Note that there is one more rule that really sets it apart from class models.
Data models are conceptually wrappers around actual data object nodes. This means that when creating an instance via `new` you are really creating a wrapper over the data node (or a new data node if it was not one). Also this means that you can't insert the model itself into a tree, but that you must insert the data being wrapped instead (accessible through `model.$`).
## Creating a data model instance
An instance of the todo data node plus its wrapper model can be created like this:
```ts
const myTodo1 = new Todo({ done: true, text: "buy some milk" })
// `myTodo1.$` will hold the data object that can be inserted into a tree
```
Note that if the input data is a tree node then `myTodo1.$` will be exactly that same data tree node passed in the constructor.
If it is not a tree node then `myTodo1.$` will be the `toTreeNode` version of the passed data object.
Also, multiple calls to `new` over a same data tree node will return the same model instance every time.
All this means that usually you will just pass the data around and only do a `new` over the data whenever you need to modify it.
Some examples:
```ts
const todoList: ModelData = [...];
// usually we would use `todoList[x]` to access the data ...
// until the moment we want to edit a particular todo
const editableTodo = new Todo(todoList[x]);
editableTodo.setText("hi there")
// once done we can just "throw away" the editable instance
```
```ts
const todoList: ModelData = [...];
const newTodo = new Todo({ done: false, text: "" })
// ...
newTodo.setText("buy milk")
// note how we insert into the tree the data, not the model itself!
todoList.push(newTodo.$);
```
## Automatic data model actions for property setters
Most times, the only action we need for a property is a setter. We can use the prop modifier `withSetter()` (`withSetter("assign")` has been deprecated) to reduce boilerplate and generate property setters. For example, the model above could be written as:
```ts
@model("myCoolApp/Todo")
export class Todo extends DataModel({
text: prop().withSetter(),
done: prop().withSetter(),
}) {}
const myTodo = new Todo({ text: "buy some coffee", done: false })
// this is now allowed and properly wrapped in two respective actions
myTodo.setText("buy some milk")
myTodo.setDone(true)
```
If for some reason you still require to change these without using a `modelAction` consider using `objectActions.set`, `objectActions.delete`, `objectActions.call`, `arrayActions.push`, etc. if needed.
`withSetter` also accepts a value transform function. For example `withSetter(cloneTreeValue)` clones incoming non-primitive values before setting them, which is useful when the same object instance may be reused across different places in the tree. If you need clone options, wrap it (for example `withSetter((x) => cloneTreeValue(x, { generateNewIds: false }))`).
Note that the transform function is only applied when using the generated setter method (for example, `model.setX(3)`); it is **not** applied during model construction (`new Model({ x: 3 })`) or snapshot deserialization, where the value is assigned as provided.
## Life-cycle event hooks
Data models only support a single limited life-cycle event hook:
- `onLazyInit()`, which is called the first time `new` is called to wrap a certain data node in the life-time of the application.
If you need something that runs more consistently consider using `onChildAttachedTo` over the data node parent itself.
## Runtime data
Runtime data (data that doesn't need to be snapshotable, or that needs to be tracked in any way) can be declared as a usual property. Nothing special is needed.
```ts
@model("myApp/SomeModel")
class SomeModel extends DataModel({
title: prop("demo"),
}) {
// non-observable runtime data
saveCount = 0
markSaved() {
this.saveCount++
}
// or observable in the usual MobX way
@observable
isSaving = false
@action
setSaving(isSaving: boolean) {
this.isSaving = isSaving
}
}
```
Note that this runtime data holds to the same lazy creation rules as the data model wrapper instance itself.
## Getting the TypeScript types for model data
- `ModelData` is the type of the model props without transformations (as accessible via `model.$`).
For example `ModelData` would return `{ text: string; done: boolean; }`.
## Flows (async actions)
While `@modelAction` defines sync model actions, async model actions are possible as well with the use of `@modelFlow`:
```ts
interface Book {
title: string
price: number
}
@model("myApp/BookStore")
class BookStore extends DataModel({
books: prop(() => []),
}) {
// TypeScript version
@modelFlow
// note: `_async` is a function that has to be imported, we have to use `this: THISCLASS`
fetchMyBooksAsync = _async(function* (this: BookStore, token: string) {
// we use `yield* _await(X)` where we would use `await X`
// note: it is `yield*`, NOT just `yield`; `_await` is a function that has to be imported
const myBooks = yield* _await(myBackendClient.getBooks(token))
this.books = myBooks
});
// JavaScript version
@modelFlow
// we use `function*` (a function generator) where we would use `async`
*fetchMyBooksAsync(token) {
// we use `yield* _await(X)` where we would use `await X`
// note: it is `yield*`, NOT just `yield`; `_await` is a function that has to be imported
const myBooks = yield* _await(myBackendClient.getBooks(token))
this.books = myBooks
}
}
// in either case it can be used like this
const myBookStore = new BookStore({})
await myBookStore.fetchMyBooksAsync("someToken")
```
## Factory pattern / Generics
If you are _not_ relying on `tProp` to do runtime type checking it is possible to use this pattern to get generic classes:
```ts
@model("myApp/GenericPoint")
class GenericPoint extends DataModel(() => ({
x: prop(),
y: prop(),
})) {
@modelAction
setXY(x: T, y: T) {
this.x = x
this.y = y
}
}
@model("myApp/Generic3dPoint")
class Generic3dPoint extends ExtendedDataModel(() => ({
baseModel: modelClass>(GenericPoint),
props: {
z: prop(),
},
})) {
// ...
}
```
If you rely on `tProp` (and also `prop` really) a different possibility is to use a factory pattern with data models. For example:
```ts
function createModelClass(modelName: string, initialX: TX, initialY: TY) {
@model(`myApp/${modelName}`)
class MyModel extends DataModel({
x: prop(() => initialX),
y: prop(() => initialY),
}) {
@modelAction
setXY(x: TX, y: TY) {
this.x = x
this.y = y
}
}
return MyModel
}
const NumberMyModel = createModelClass("NumberMyModel", 10, 20)
type NumberMyModel = InstanceType
const numberMyModelInstance = new NumberMyModel({}) // this will be of type `NumberMyModel`
numberMyModelInstance.setXY(50, 60)
const StringMyModel = createModelClass("StringMyModel", "10", "20")
type StringMyModel = InstanceType
const stringMyModelInstance = new StringMyModel({}) // this will be of type`StringMyModel`
stringMyModelInstance.setXY("50", "60")
```
Note that the above will only work when not generating declaration maps. If you need to generate declarations (for example for a library) then it is a bit more tedious, but still possible:
```ts
export function createModelClass(modelName: string, initialX: TX, initialY: TY) {
const MyModelProps = DataModel({
x: prop(() => initialX),
y: prop(() => initialY),
})
@model(`myApp/${modelName}`)
class MyModel extends MyModelProps {
@modelAction
setXY(x: TX, y: TY) {
this.x = x
this.y = y
}
}
return MyModel as ModelClassDeclaration<
typeof MyModelProps,
{
setXY(x: TX, y: TY): void
}
>
}
```
## Inheritance
Model inheritance is possible with a few gotchas.
The first thing to bear in mind is that data models that extend from other data models must use `ExtendedDataModel` rather than the plain `DataModel`. For example:
```ts
@model("MyApp/Point")
class Point extends DataModel({
x: prop(),
y: prop(),
}) {
get sum() {
return this.x + this.y
}
}
// note how `ExtendedModel` is used
@model("MyApp/Point3d")
class Point3d extends ExtendedDataModel(Point, {
z: prop(),
}) {
get sum() {
return super.sum + this.z
}
}
```
Also, remember that if your base model has `onLazyInit` and you redeclare it in your extended model you will need to call `super.onLazyInit(...)` in the extended model.
If you want to extend a generic class, then you may want to use `modelClass` in order to specify the exact generic like this:
```ts
class X extends ExtendedDataModel(modelClass>(SomeGenericClass), { ... }) { ... }
```
If you don't it will still compile, but the generic will be assumed to have `unknown` for all its generic parameters.
## Usage without decorators
Although this library was primarily intented to be used with decorators it is also possible to use it without them.
To do so you can use the `decoratedModel` function as shown below:
```ts
// note the `_` at the beginning of the name to distinguish it from the decorated version
class _Todo extends DataModel({
text: prop(),
done: prop(),
}) {
// note how here we don't decorate the method directly, but on the next parameter instead
// @modelAction
setDone(done: boolean) {
this.done = done
}
// @modelAction
setText(text: string) {
this.text = text
}
// @computed
get fullText() {
return `${this.done ? "DONE" : "TODO"} - ${this.text}`
}
}
const Todo = decoratedModel(
// the string identifies this model type and must be unique across your whole application
// you may pass `undefined` if you don't want the model to be registered yet (e.g. for a base class)
"myCoolApp/Todo",
_Todo,
,
// here we pass what we would use as decorators to the class methods/properties above
// if we want to use multiple chained decorators we can pass an array of them instead
// note that any kind of TypeScript-compatible decorator is supported, not only the built-in ones!
{
setDone: modelAction,
setText: modelAction,
fullText: computed,
}
)
// needed to be able to do `SnapshotInOf`, type a variable as `Todo`, etc
type Todo = _Todo
// if `_Todo` was generic then it would be `type Todo = _Todo`
const myTodo = new Todo({ done: false, text: "buy some milk" })
```
---
# standardAndStandaloneActions
## Standalone Actions
Sometimes you might need to define a "model" action but without an associated model. Say for example that you need an array swap method that needs to be processed by middlewares (e.g. [`undoMiddleware`](./actionMiddlewares/undoMiddleware.mdx)). One way to achieve this is to use standalone actions like this:
```ts
const arraySwap = standaloneAction(
"myApp/arraySwap",
(array: T[], index1: number, index2: number): void => {
if (index2 < index1) {
;[index1, index2] = [index2, index1]
}
// since a same node cannot be in two places at once we will remove
// both then reinsert them
const [v1] = array.splice(index1, 1)
const [v2] = array.splice(index2 - 1, 1)
array.splice(index1, 0, v2)
array.splice(index2, 0, v1)
}
)
```
Note the following prerequisites apply to standalone actions:
- The name provided must be unique across your whole application.
- The first argument (the target) must always be an existing tree node.
### `standaloneFlow`
If the same idea needs asynchronous steps, use `standaloneFlow`. It follows the same rules as `standaloneAction`, but behaves like a flow and returns a promise:
```ts
const renameAfterSave = standaloneFlow(
"myApp/renameAfterSave",
function* (todo: Todo, newText: string) {
yield api.saveTodo(todo.id, { text: newText })
todo.setText(newText)
}
)
```
This is useful when you want middleware support for reusable async logic without attaching that logic to a specific model class.
## Standard Actions
In order to work over objects and arrays without requiring declaring custom actions you can use the already predefined `objectActions` and `arrayActions` (note these also work over class models).
`objectActions` work over any kind of object (including models themselves) and offer:
- `set(obj, key, value)` to set a key.
- `delete(obj, key)` to delete a key.
- `assign(obj, partialObj)` to assign values (similar to `Object.assign`).
- `call(obj, methodName, ...args)` to call a method.
`arrayActions` work over arrays and offer:
- `set(array, index, value)` to set an index.
- `delete(array, index)` to delete an index.
- `setLength(array, length)` to set a new length.
- `swap(array, index1, index2)` to swap two array elements.
Plus the usual array mutation methods (`pop`, `push`, etc.).
---
# treeLikeStructure
## Overview
`mobx-keystone`'s structure is based on a tree-like structure, where each node can be one of:
- A model instance.
- A plain object.
- An array.
- A primitive value (`string`, `boolean`, `number`, `null`, `undefined`).
By default, arrays _cannot_ hold `undefined` values, but they _can_ hold `null` values. This rule exists for JSON compatibility. If you really need arrays with `undefined` values, you can enable them in the global configuration:
```ts
setGlobalConfig({
allowUndefinedArrayElements: true,
})
```
Since the structure is a tree, this means these tree rules apply:
1. A non-primitive (object) node can have zero or one parent.
2. A non-primitive (object) node can have zero to infinite children.
3. From rules 1 and 2 we can extract that the same non-primitive node can only be in a single tree and only once.
4. Primitive nodes are always copied by value, so none of the rules above apply.
5. Note that class models with the `valueType: true` option will get cloned automatically before getting inserted as a child of another node so, for all practical purposes, rule 3 does not apply and acts more akin to a primitive.
As an example of rule 1, this would not be allowed:
```ts
// given `someModel`, `someOtherModel`, `someArray`
// ok, `someArray` has now one parent and becomes a tree node object
someModel.setArray(someArray)
// but this would throw since `someArray` is already a tree node object which already has one parent
someOtherModel.setArray(someArray)
```
But as rule 4 states, this would be ok:
```ts
// given `someModel`, `someOtherModel`
const somePrimitive = "hi!"
// ok, the primitive is copied, and has now one parent
someModel.setPrimitive(somePrimitive)
// ok too, since the primitive is copied again, and has one parent
someOtherModel.setPrimitive(somePrimitive)
```
A way to work around rule 1 is possible thanks to the use of references as shown in the [references](./references.mdx) section.
## How objects are transformed into nodes
A model/object/array is turned into a tree node under the following circumstances:
- Model instances are _always_ tree nodes.
- Plain objects / arrays are turned into tree nodes as soon as they become children of another tree node.
To check if a non-primitive has been turned into a tree node you can use `isTreeNode(value: object): boolean`, or
`assertIsTreeNode(value: object, argName: string = "argument"): asserts value is object` to assert it.
To turn a non-primitive into a tree node you can use `toTreeNode(value: T): T`. If the object is already a tree node then the same object will be returned.
Additionally, `toTreeNode(type: TType, value: V): V` can be used with a type checker which will be invoked to check the data (when auto model type checking is enabled) if desired.
## Traversal methods
When a non-primitive value is turned into a tree node it gains access to certain methods that allow traversing the data tree:
### `getParentPath`
```ts
getParentPath(value: object): ParentPath | undefined
```
Returns the parent of the target plus the path from the parent to the target, or `undefined` if it has no parent.
### `getParent`
```ts
getParent(value: object): T | undefined
```
Returns the parent object of the target object, or `undefined` if there's no parent.
### `getParentToChildPath`
```ts
getParentToChildPath(fromParent: object, toChild: object): Path | undefined
```
Gets the path to get from a parent to a given child.
Returns an empty array if the child is actually the given parent or `undefined` if the child is not a child of the parent.
### `isModelDataObject`
```ts
isModelDataObject(value: object): boolean
```
Returns `true` if a given object is a model interim data object (`$`).
### `getRootPath`
```ts
getRootPath(value: object): RootPath
```
Returns the root of the target, the path from the root to get to the target and the list of objects from root (included) until target (included).
### `getRoot`
```ts
getRoot(value: object): T
```
Returns the root of the target object, or itself if the target is a root.
:::warning Detached nodes
If a node gets detached from its parent tree, `getRoot(node)` will return that same node.
This matters in reactive code (`computed`, `autorun`, etc.). If a computed uses `getRoot(this)` and then reads a property that resolves back to the same computed, MobX will throw a cycle error.
When you need to access an actual registered root store, prefer `getRootStore` and guard for `undefined`:
```ts
const rootStore = getRootStore(this)
if (!rootStore) {
return undefined
}
return rootStore.someValue
```
:::
### `isRoot`
```ts
isRoot(value: object): boolean
```
Returns `true` if a given object is a root object.
### `isChildOfParent`
```ts
isChildOfParent(child: object, parent: object): boolean
```
Returns `true` if the target is a "child" of the tree of the given "parent" object.
### `isParentOfChild`
```ts
isParentOfChild(parent: object, child: object): boolean
```
Returns `true` if the target is a "parent" that has in its tree the given "child" object.
### `resolvePath`
```ts
resolvePath(pathRootObject: object, path: Path): { resolved: true; value: T } | { resolved: false }
```
Resolves a path from an object, returning an object with `{ resolved: true, value: T }` or `{ resolved: false }`.
### `findParent`
```ts
findParent(child: object, predicate: (parent: object) => boolean, maxDepth = 0): T | undefined
```
Iterates through all the parents (from the nearest until the root) until one of them matches the given predicate.
If the predicate is matched it will return the found node.
If none is found it will return `undefined`.
A max depth of 0 is infinite, but another one can be given.
### `findParentPath`
```ts
findParentPath(child: object, predicate: (parent: object) => boolean, maxDepth = 0): FoundParentPath | undefined
```
Iterates through all the parents (from the nearest until the root) until one of them matches the given predicate.
If the predicate is matched it will return the found node and the path from the parent to the child.
If none is found it will return `undefined`.
A max depth of 0 is infinite, but another one can be given.
### `findChildren`
```ts
findChildren(root: object, predicate: (node: object) => boolean, options?: { deep?: boolean }): ReadonlySet
```
Iterates through all children and collects them in a set if the given predicate matches.
Pass the options object with the `deep` option (defaults to `false`) set to `true` to get the children deeply or `false` to get them shallowly.
### `getChildrenObjects`
```ts
getChildrenObjects(node: object, options?: { deep?: boolean }): ReadonlySet