Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix typescript types after type optimization and cast, add castToSnapshot and castToReferenceSnapshot #1074

Merged
merged 12 commits into from
Nov 19, 2018
913 changes: 497 additions & 416 deletions API.md

Large diffs are not rendered by default.

36 changes: 28 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -924,10 +924,10 @@ Note that since MST v3 `types.array` and `types.map` are wrapped in `types.optio
- `types.late(() => type)` can be used to create recursive or circular types, or types that are spread over files in such a way that circular dependencies between files would be an issue otherwise.
- `types.frozen(subType? | defaultValue?)` Accepts any kind of serializable value (both primitive and complex), but assumes that the value itself is **immutable** and **serializable**.
`frozen` can be invoked in a few different ways:
- `types.frozen()` - behaves the same as types.frozen in MST 2.
- `types.frozen(subType)` - provide a valid MST type and frozen will check if the provided data conforms the snapshot for that type. Note that the type will not actually be instantiated, so it can only be used to check the shape of the data. Adding views or actions to SubType would be pointless.
- `types.frozen(someDefaultValue)` - provide a primitive value, object or array, and MST will infer the type from that object, and also make it the default value for the field
- (Typescript) `types.frozen<TypeScriptType>(...)` - provide a typescript type, to help in strongly typing the field (design time only)
- `types.frozen()` - behaves the same as types.frozen in MST 2.
- `types.frozen(subType)` - provide a valid MST type and frozen will check if the provided data conforms the snapshot for that type. Note that the type will not actually be instantiated, so it can only be used to check the shape of the data. Adding views or actions to SubType would be pointless.
- `types.frozen(someDefaultValue)` - provide a primitive value, object or array, and MST will infer the type from that object, and also make it the default value for the field
- (Typescript) `types.frozen<TypeScriptType>(...)` - provide a typescript type, to help in strongly typing the field (design time only)
- `types.compose(name?, type1...typeX)`, creates a new model type by taking a bunch of existing types and combining them into a new one.

## Property types
Expand Down Expand Up @@ -993,8 +993,10 @@ See the [full API docs](API.md) for more details.
| [`addMiddleware(node, middleware: (actionDescription, next) => any, includeHooks)`](API.md#addmiddleware) | Attaches middleware to a node. See [middleware](docs/middleware.md). Returns disposer. |
| [`applyAction(node, actionDescription)`](API.md#applyaction) | Replays an action on the targeted node |
| [`applyPatch(node, jsonPatch)`](API.md#applypatch) | Applies a JSON patch, or array of patches, to a node in the tree |
| [`cast(nodeOrSnapshot)`](API.md#cast) | Cast a node instance or snapshot to a node so it can be used in assignment operations |
| [`applySnapshot(node, snapshot)`](API.md#applysnapshot) | Updates a node with the given snapshot |
| [`cast(nodeOrSnapshot)`](API.md#cast) | Cast a node instance or snapshot to a node instance so it can be used in assignment operations |
| [`castToSnapshot(nodeOrSnapshot)`](API.md#casttosnapshot) | Cast a node instance to a snapshot so it can be used inside create operations |
| [`castToReferenceSnapshot(node)`](API.md#casttoreferencesnapshot) | Cast a node instance to a reference snapshot so it can be used inside create operations |
| [`createActionTrackingMiddleware`](API.md#createactiontrackingmiddleware) | Utility to make writing middleware that tracks async actions less cumbersome |
| [`clone(node, keepEnvironment?: true \| false \| newEnvironment)`](API.md#clone) | Creates a full clone of the given node. By default preserves the same environment |
| [`decorate(handler, function)`](API.md#decorate) | Attaches middleware to a specific action (or flow) |
Expand Down Expand Up @@ -1436,14 +1438,32 @@ s.replaceTasks([{ done: true }])
s.replaceTasks(types.array(Task).create([{ done: true }]))
```

Additionally, the `cast` function can be also used in the inverse case, this is when you want to use an instance inside an snapshot.
In this case MST will internally convert the instance to an snapshot before using it, but we need once more to fool TypeScript into
Additionally, the `castToSnapshot` function can be also used in the inverse case, this is when you want to use an instance inside an snapshot.
In this case MST will internally convert the instance to a snapshot before using it, but we need once more to fool TypeScript into
thinking that this instance is actually a snapshot.

```typescript
const task = Task.create({ done: true })
const Store = types.model({
tasks: types.array(Task)
})

// we cast the task instance to a snapshot so it can be used as part of another snapshot without typing errors
const s = Store.create({ tasks: [cast(task)] })
const s = Store.create({ tasks: [castToSnapshot(task)] })
```

Finally, the `castToReferenceSnapshot` can be used when we want to use an instance to actually use a reference snapshot (a string or number).
In this case MST will internally convert the instance to a reference snapshot before using it, but we need once more to fool TypeScript into
thinking that this instance is actually a snapshot of a reference.

```typescript
const task = Task.create({ id: types.identifier, done: true })
const Store = types.model({
tasks: types.array(types.reference(Task))
})

// we cast the task instance to a reference snapshot so it can be used as part of another snapshot without typing errors
const s = Store.create({ tasks: [castToReferenceSnapshot(task)] })
```

#### Known Typescript Issue 5938
Expand Down
3 changes: 3 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# 3.8.0

- Added castToSnapshot/castToReferenceSnapshot methods for TypeScript and fixed some TypeScript typings not being properly detected when using SnapshotIn types through [#1074](https://github.com/mobxjs/mobx-state-tree/pull/1074) by [@xaviergonz](https://github.com/xaviergonz)
- Fixed redux middleware throwing an error when a flow is called before it is connected [#1065](https://github.com/mobxjs/mobx-state-tree/issues/1065) through [#1079](https://github.com/mobxjs/mobx-state-tree/pull/1079) by [@mkramb](https://github.com/mkramb) and [@xaviergonz](https://github.com/xaviergonz)
- Made `addDisposer` return the passed disposer through [#1059](https://github.com/mobxjs/mobx-state-tree/pull/1059) by [@xaviergonz](https://github.com/xaviergonz)

Expand Down
9 changes: 5 additions & 4 deletions packages/mobx-state-tree/__tests__/core/action.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
getRoot,
cast,
IMiddlewareEvent,
ISerializedActionCall
ISerializedActionCall,
Instance
} from "../../src"

/// Simple action replay and invocation
Expand Down Expand Up @@ -93,10 +94,10 @@ const Order = types
customer: types.maybeNull(types.reference(Customer))
})
.actions(self => {
function setCustomer(customer: typeof Customer.Type) {
function setCustomer(customer: Instance<typeof Customer>) {
self.customer = customer
}
function noopSetCustomer(_: typeof Customer.Type) {
function noopSetCustomer(_: Instance<typeof Customer>) {
// noop
}
return {
Expand Down Expand Up @@ -390,7 +391,7 @@ test("after attach action should work correctly", () => {
todos: types.array(Todo)
})
.actions(self => ({
remove(todo: typeof Todo.Type) {
remove(todo: Instance<typeof Todo>) {
self.todos.remove(todo)
}
}))
Expand Down
2 changes: 2 additions & 0 deletions packages/mobx-state-tree/__tests__/core/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ const METHODS_AND_INTERNAL_TYPES = stringToArray(`
getMembers,
getPropertyMembers,
cast,
castToSnapshot,
castToReferenceSnapshot,
isType,
isArrayType,
isFrozenType,
Expand Down
8 changes: 4 additions & 4 deletions packages/mobx-state-tree/__tests__/core/array.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ test("it should create a factory", () => {
})
test("it should succeed if not optional and no default provided", () => {
const Factory = types.array(types.string)
expect(Factory.create().toJSON!()).toEqual([])
expect(getSnapshot(Factory.create())).toEqual([])
})
test("it should restore the state from the snapshot", () => {
const { Factory } = createTestFactories()
Expand Down Expand Up @@ -147,13 +147,13 @@ test("paths shoud remain correct when splicing", () => {
})
unprotect(store)
expect(store.todos.map(getPath)).toEqual(["/todos/0"])
store.todos.push(cast({}))
store.todos.push({})
expect(store.todos.map(getPath)).toEqual(["/todos/0", "/todos/1"])
store.todos.unshift(cast({}))
store.todos.unshift({})
expect(store.todos.map(getPath)).toEqual(["/todos/0", "/todos/1", "/todos/2"])
store.todos.splice(0, 2)
expect(store.todos.map(getPath)).toEqual(["/todos/0"])
store.todos.splice(0, 1, cast({}), cast({}), cast({}))
store.todos.splice(0, 1, {}, {}, {})
expect(store.todos.map(getPath)).toEqual(["/todos/0", "/todos/1", "/todos/2"])
store.todos.remove(store.todos[1])
expect(store.todos.map(getPath)).toEqual(["/todos/0", "/todos/1"])
Expand Down
14 changes: 11 additions & 3 deletions packages/mobx-state-tree/__tests__/core/boxes-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@
* Based on examples/boxes/domain-state.js
*/
import { values } from "mobx"
import { types, getParent, hasParent, recordPatches, unprotect, getSnapshot } from "../../src"
import {
types,
getParent,
hasParent,
recordPatches,
unprotect,
getSnapshot,
Instance
} from "../../src"

export const Box = types
.model("Box", {
Expand Down Expand Up @@ -56,15 +64,15 @@ export const Store = types
function addArrow(id: string, from: string, to: string) {
self.arrows.push(Arrow.create({ id, from, to }))
}
function setSelection(selection: typeof Box.Type) {
function setSelection(selection: Instance<typeof Box>) {
self.selection = selection
}
function createBox(
id: string,
name: string,
x: number,
y: number,
source: typeof Box.Type | null | undefined,
source: Instance<typeof Box> | null | undefined,
arrowId: string | null
) {
const box = addBox(id, name, x, y)
Expand Down
8 changes: 4 additions & 4 deletions packages/mobx-state-tree/__tests__/core/custom-type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class Decimal {
const b1 = w1.balance
expect(b1).toBeInstanceOf(Decimal)

w1.balance = cast("2.5")
w1.balance = "2.5" as any // TODO: make cast work with custom types
expect(b1).toBeInstanceOf(Decimal)
expect(w1.balance).toBe(b1) // reconciled

Expand All @@ -93,7 +93,7 @@ class Decimal {
w1.balance = new Decimal("3.5")
expect(b1).toBeInstanceOf(Decimal)

w1.balance = cast("4.5")
w1.balance = "4.5" as any
expect(b1).toBeInstanceOf(Decimal)

w1.lastTransaction = b1
Expand Down Expand Up @@ -163,7 +163,7 @@ class Decimal {
const b1 = w1.balance
expect(b1).toBeInstanceOf(Decimal)

w1.balance = cast([2, 5])
w1.balance = [2, 5] as any
expect(b1).toBeInstanceOf(Decimal)
expect(w1.balance).not.toBe(b1) // not reconciled, balance is not deep equaled (TODO: future feature?)

Expand All @@ -173,7 +173,7 @@ class Decimal {
w1.balance = new Decimal("3.5")
expect(b1).toBeInstanceOf(Decimal)

w1.balance = cast([4, 5])
w1.balance = [4, 5] as any
expect(b1).toBeInstanceOf(Decimal)

// patches & snapshots
Expand Down
2 changes: 1 addition & 1 deletion packages/mobx-state-tree/__tests__/core/identifier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ if (process.env.NODE_ENV !== "production") {
})
.actions(self => ({
addModel(model: SnapshotOrInstance<typeof Model>) {
self.models.push(cast(model))
self.models.push(model)
}
}))
expect(() => {
Expand Down
2 changes: 1 addition & 1 deletion packages/mobx-state-tree/__tests__/core/jsonpatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ test("it should apply deep patches to objects", () => {
NodeObject,
{ id: 1, child: { id: 2 } },
(n: Instance<typeof NodeObject>) => {
n.child.text = "test" // update
n.child!.text = "test" // update
n.child = cast({ id: 2, text: "world" }) // this reconciles; just an update
n.child = NodeObject.create({ id: 2, text: "coffee", child: { id: 23 } })
n.child = cast({ id: 3, text: "world", child: { id: 7 } }) // addition
Expand Down
4 changes: 2 additions & 2 deletions packages/mobx-state-tree/__tests__/core/late.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ test("should typecheck", () => {
})
const x = NodeObject.create({ id: 1 })
try {
x.child = 3 // TODO: better typings, should give compilation error!
;(x as any).child = 3
;(x as any).floepie = 3
} catch (e) {
// ignore, this is about TS
Expand Down Expand Up @@ -126,5 +126,5 @@ test("#916 - 3", () => {
newTodo: { title: "test" }
})

expect(t.newTodo.title).toBe("test")
expect(t.newTodo!.title).toBe("test")
})
2 changes: 1 addition & 1 deletion packages/mobx-state-tree/__tests__/core/object.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ test("it should create a factory", () => {
const instance = Factory.create()
const snapshot = getSnapshot(instance)
expect(snapshot).toEqual({ to: "world" })
expect(Factory.create().toJSON!()).toEqual({ to: "world" }) // toJSON is there as shortcut for getSnapshot(), primarily for debugging convenience
expect(getSnapshot(Factory.create())).toEqual({ to: "world" }) // toJSON is there as shortcut for getSnapshot(), primarily for debugging convenience
expect(Factory.create().toString()).toEqual("AnonymousModel@<root>")
})
test("it should restore the state from the snapshot", () => {
Expand Down
18 changes: 8 additions & 10 deletions packages/mobx-state-tree/__tests__/core/parent-properties.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,16 +95,14 @@ test("#917", () => {
}))
.actions(self => ({
addTodo(title: string) {
self.todos.push(
cast({
title,
subTodos: [
{
title
}
]
})
)
self.todos.push({
title,
subTodos: [
{
title
}
]
})
}
}))

Expand Down
6 changes: 3 additions & 3 deletions packages/mobx-state-tree/__tests__/core/pointer.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { types, unprotect, IAnyModelType } from "../../src"
import { types, unprotect, IAnyModelType, castToReferenceSnapshot } from "../../src"

function Pointer<IT extends IAnyModelType>(Model: IT) {
return types.model("PointerOf" + Model.name, {
Expand All @@ -20,7 +20,7 @@ test("it should allow array of pointer objects", () => {
selected: []
})
unprotect(store)
const ref = TodoPointer.create({ value: store.todos[0] }) // Fails because store.todos does not belongs to the same tree
const ref = TodoPointer.create({ value: castToReferenceSnapshot(store.todos[0]) }) // Fails because store.todos does not belongs to the same tree
store.selected.push(ref)
expect(store.selected[0].value).toBe(store.todos[0])
})
Expand Down Expand Up @@ -51,7 +51,7 @@ test("it should allow array of pointer objects - 3", () => {
selected: []
})
unprotect(store)
const ref = TodoPointer.create({ value: store.todos[0] })
const ref = TodoPointer.create({ value: castToReferenceSnapshot(store.todos[0]) })
store.selected.push(ref)
expect(store.selected[0].value).toBe(store.todos[0])
})
Expand Down
2 changes: 1 addition & 1 deletion packages/mobx-state-tree/__tests__/core/protect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ test("protect should protect against any update", () => {
"[mobx-state-tree] Cannot modify 'Todo@<root>', the object is protected and can only be modified by using an action."
)
expect(() => {
store.todos.push(cast({ title: "test" }))
store.todos.push({ title: "test" })
}).toThrowError(
"[mobx-state-tree] Cannot modify 'Todo[]@/todos', the object is protected and can only be modified by using an action."
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ test("it should support dynamic loading", done => {
.actions(self => ({
loadUser: flow(function* loadUser(name: string) {
events.push("loading " + name)
self.users.push(cast({ name }))
self.users.push({ name })
yield new Promise(resolve => {
setTimeout(resolve, 200)
})
Expand Down
Loading