Skip to content

Commit

Permalink
Improve definition of recursive models.
Browse files Browse the repository at this point in the history
  • Loading branch information
jjrv committed Apr 27, 2018
1 parent 15205b3 commit d9985e2
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 74 deletions.
105 changes: 37 additions & 68 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ The `mst` function binds the two together (producing a new type "inheriting"
`TodoData`), and the `TodoCode` class should not be used directly.
A third, optional parameter gives the resulting model a name.
Names are required for polymorphism to work correctly, when serializing
models to JSON containing fields supporting different possible subclasses.
models to JSON containing fields with types that have further subclasses.

The `shim` function is a tiny wrapper that makes TypeScript accept MST types
as superclasses. It must be used in the `extends` clause of the ES6 class
Expand Down Expand Up @@ -145,7 +145,7 @@ before the first parent class instance has been created anywhere in the program.
Snapshots containing polymorphic types require type names in the serialized JSON,
to identify the correct subclass when applying the snapshot.
A special key `$` is automatically added in snapshots when an object in the tree
belongs to a subclass of the class actually defined in the model.
belongs to a subclass of the class actually specified in the model.

The default key `$` for types can be changed by passing a different string to the
`setTypeTag` function before creating any model instances. For example:
Expand Down Expand Up @@ -269,88 +269,57 @@ Fully typed recursive types require some tricky syntax to avoid these TypeScript
- `error TS2506: 'Type' is referenced directly or indirectly in its own base expression.`
- `error TS7022: 'Type' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.`

Luckily interface types are lazier so they support recursive references.
First we can define the type with nice syntax, exactly as it should ideally work:
If your model has a `children` property containing an array of the same model
as their parent, the easiest solution is to add the `children` property only
in the ES6 class and use `mstWithChildren` instead of `mst` when defining the
model. It handles adding the property to the `mobx-state-tree` type.

```TypeScript
import { IObservableArray } from 'mobx';
import { types, ISnapshottable, IModelType, IComplexType } from 'mobx-state-tree';
import { mst, shim, action, ModelInterface } from 'classy-mst';
The function `mstWithChildren` returns an object with the members:

export const NodeData = types.model({
- `Model`, the model with views, actions and a `children` property attached.
- `Children`, the correct `mobx-state-tree` type for the `children` property.

// Non-recursive members go here, for example:
id: ''

});
You should call it just after your class defining the views and actions
(replacing Todo with your own class name) like this:

export class NodeCode extends shim(NodeData) {

// Example method. Note how all members are available and fully typed,
// even if recursively defined.

getChildIDs() {
for(let child of this.children || []) {
if(child.children) child.getChildIDs();
if(child.id) console.log(child.id);
}
}

// Recursive members go here first.
children?: Node[];

}
```TypeScript
const { Model: Todo, Children } = mstWithChildren(TodoCode, TodoData, 'Todo');
```

Then we need unfortunate boilerplate to make the compiler happy:
You can use the `Children` type inside the class methods thanks to declaration
hoisting. Without the type, it's difficult to initialize an unset `children`
property correctly.

The `children` property should be declared in your class as
`(this | <class name>)[]` to allow further inheritance, like this:

```TypeScript
export const NodeBase = mst(NodeCode, NodeData);
export type NodeBase = typeof NodeBase.Type;
import { IObservableArray } from 'mobx';
import { types, ISnapshottable, IModelType, IComplexType } from 'mobx-state-tree';
import { mst, mstWithChildren, shim, action, ModelInterface } from 'classy-mst';

// Interface trickery to avoid compiler errors when defining a recursive type.
export interface NodeObservableArray extends IObservableArray<NodeRecursive> {}
export const NodeData = T.model({ value: 42 });
export class NodeCode extends shim(NodeData) {

export interface NodeRecursive extends NodeBase {
@action
addChild(value = 42) {
if(!this.children) this.children = Children.create();
this.children.push(Node.create());

// Recursive members go here second.
children: NodeObservableArray
return(this);
}

children?: (this | NodeCode)[];
}

export type NodeArray = IComplexType<
(typeof NodeBase.SnapshotType & {

// Recursive members go here third.
children: any[]

})[],
NodeObservableArray
>;

export const Node = NodeBase.props({

// Recursive members go here fourth.
children: types.maybe(types.array(types.late((): any => Node)) as NodeArray),

});

export type Node = typeof Node.Type;
const { Model: Node, Children } = mstWithChildren(NodeCode, NodeData, 'Node');
```

Finally, the new type can be used like this:

```TypeScript
const tree = Node.create({
children: [
{ children: [ { id: 'TEST' } ] }
]
});

// Both print: TEST
console.log(tree.children![0].children![0].id);
tree.getChildIDs();
```
If you want to use some other name than `children` for the property, easiest is
to copy, paste and customize the `mstWithChildren` function from
[classy-mst.ts](https://github.com/charto/classy-mst/blob/master/src/classy-mst.ts).
Without macro support in the TypeScript compiler, the name cannot be
parameterized while keeping the code fully typed.

License
=======
Expand Down
42 changes: 37 additions & 5 deletions src/classy-mst.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// This file is part of classy-mst, copyright (c) 2017-2018 BusFaster Ltd.
// Released under the MIT license, see LICENSE.

import { IType, IModelType, IStateTreeNode, types } from 'mobx-state-tree';
import { IObservableArray } from 'mobx';
import { IType, IModelType, IComplexType, IStateTreeNode, ISnapshottable, types } from 'mobx-state-tree';

/** Fake complete, generic implementation of IModelType. */

Expand Down Expand Up @@ -96,8 +97,9 @@ export function mst<S, T, U>(Code: new() => U, Data: IModelType<S, T>, name?: st
// defined in the constructor.

const instance: { [name: string]: any } = new Code();
const volatileList = Object.getOwnPropertyNames(instance);

for(let name of Object.getOwnPropertyNames(instance)) {
for(let name of volatileList) {
volatileTbl[name] = instance[name];
}

Expand All @@ -110,7 +112,9 @@ export function mst<S, T, U>(Code: new() => U, Data: IModelType<S, T>, name?: st
let Model = Data.preProcessSnapshot(
// Instantiating a union of models requires a snapshot.
(snap: any) => snap || {}
).views((self) => {
);

Model = !(viewList.length + descList.length) ? Model : Model.views((self) => {
const result: { [name: string]: Function } = {};

for(let { name, value } of viewList) {
Expand All @@ -129,7 +133,9 @@ export function mst<S, T, U>(Code: new() => U, Data: IModelType<S, T>, name?: st
}

return(result);
}).actions((self) => {
});

Model = !actionList.length ? Model : Model.actions((self) => {
const result: { [name: string]: Function } = {
postProcessSnapshot: (snap: any) => {
if(name && typeTag && Code.prototype.$parent) snap[typeTag] = name;
Expand All @@ -144,8 +150,14 @@ export function mst<S, T, U>(Code: new() => U, Data: IModelType<S, T>, name?: st
}

return(result);
}).volatile((self) => volatileTbl);
});

Model = !volatileList.length ? Model : Model.volatile((self) => volatileTbl);

return(polymorphic(Code, Model, name));
}

export function polymorphic<S, T, U>(Code: new() => U, Model: IModelType<S, T>, name?: string): IModelType<S, U> {
// Union of this class and all of its subclasses.
// Late evaluation allows subclasses to add themselves to the type list
// before any instances are created.
Expand Down Expand Up @@ -193,3 +205,23 @@ export function mst<S, T, U>(Code: new() => U, Data: IModelType<S, T>, name?: st

return(Union);
}

export function mstWithChildren<S, T, U extends T>(
Code: new() => U,
Data: IModelType<S, T>,
name?: string
) {
const Children = types.array(types.late((): any => Model));
const Branch = (Data as any as IModelType<S, U>).props({
children: types.maybe(
Children as IComplexType<
(S & { children?: any[] | null })[],
IObservableArray<IModelType<{}, {}>>
>
)
});

const Model = mst(Code, Branch, 'Node') as typeof Branch;

return({ Model, Children });
}
10 changes: 9 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
// This file is part of classy-mst, copyright (c) 2017-2018 BusFaster Ltd.
// Released under the MIT license, see LICENSE.

export { mst, shim, action, setTypeTag, ModelInterface } from './classy-mst';
export {
mst,
shim,
action,
polymorphic,
setTypeTag,
mstWithChildren,
ModelInterface
} from './classy-mst';

0 comments on commit d9985e2

Please sign in to comment.