mobx-keystone 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +1 -1
  3. package/dist/mobx-keystone.esm.js +555 -516
  4. package/dist/mobx-keystone.esm.mjs +555 -516
  5. package/dist/mobx-keystone.umd.js +558 -519
  6. package/dist/types/actionMiddlewares/undoMiddleware.d.ts +2 -10
  7. package/dist/types/modelShared/modelDecorator.d.ts +5 -2
  8. package/dist/types/modelShared/prop.d.ts +42 -29
  9. package/dist/types/ref/Ref.d.ts +1 -4
  10. package/dist/types/snapshot/SnapshotOf.d.ts +4 -4
  11. package/dist/types/snapshot/applySnapshot.d.ts +1 -1
  12. package/dist/types/snapshot/fromSnapshot.d.ts +2 -2
  13. package/dist/types/snapshot/getSnapshot.d.ts +1 -1
  14. package/dist/types/types/SnapshotTypeMismatchError.d.ts +3 -1
  15. package/dist/types/types/index.d.ts +1 -0
  16. package/dist/types/types/modelTypeMatching.d.ts +1 -0
  17. package/dist/types/types/schemas.d.ts +41 -37
  18. package/dist/types/types/tProp.d.ts +12 -6
  19. package/dist/types/types/types.d.ts +6 -6
  20. package/dist/types/types/utility/typeToStoredData.d.ts +14 -10
  21. package/dist/types/types/utility/typesCodecCore.d.ts +3 -0
  22. package/dist/types/utils/ModelPool.d.ts +0 -1
  23. package/dist/types/utils/types.d.ts +6 -0
  24. package/dist/types/wrappers/ArraySet.d.ts +1 -0
  25. package/package.json +13 -11
  26. package/src/action/modelFlow.ts +35 -66
  27. package/src/actionMiddlewares/actionTrackingMiddleware.ts +41 -71
  28. package/src/actionMiddlewares/transactionMiddleware.ts +1 -1
  29. package/src/actionMiddlewares/undoMiddleware.ts +16 -22
  30. package/src/context/context.ts +15 -36
  31. package/src/dataModel/utils.ts +5 -28
  32. package/src/deepChange/onDeepChange.ts +44 -126
  33. package/src/model/BaseModel.ts +1 -1
  34. package/src/model/utils.ts +5 -10
  35. package/src/modelShared/modelDecorator.ts +13 -10
  36. package/src/modelShared/modelInfo.ts +75 -0
  37. package/src/modelShared/prop.ts +115 -111
  38. package/src/modelShared/sharedInternalModel.ts +2 -2
  39. package/src/parent/arrayPaths.ts +1 -4
  40. package/src/parent/path.ts +0 -10
  41. package/src/patch/patchRecorder.ts +13 -28
  42. package/src/ref/core.ts +6 -19
  43. package/src/rootStore/rootStore.ts +1 -3
  44. package/src/snapshot/SnapshotOf.ts +24 -48
  45. package/src/snapshot/applySnapshot.ts +1 -1
  46. package/src/snapshot/fromSnapshot.ts +10 -3
  47. package/src/snapshot/getSnapshot.ts +1 -1
  48. package/src/snapshot/internal.ts +0 -2
  49. package/src/snapshot/reconcileModelSnapshot.ts +5 -3
  50. package/src/snapshot/reconcilePlainObjectSnapshot.ts +3 -1
  51. package/src/treeUtils/draft.ts +6 -1
  52. package/src/treeUtils/sandbox.ts +10 -7
  53. package/src/tweaker/tweakArray.ts +20 -50
  54. package/src/tweaker/tweakPlainObject.ts +9 -15
  55. package/src/types/SnapshotTypeMismatchError.ts +3 -1
  56. package/src/types/arrayBased/typesArray.ts +13 -19
  57. package/src/types/arrayBased/typesTuple.ts +16 -25
  58. package/src/types/createCachedTypeCheck.ts +17 -30
  59. package/src/types/index.ts +1 -0
  60. package/src/types/modelTypeMatching.ts +276 -0
  61. package/src/types/objectBased/typesDataModelData.ts +7 -8
  62. package/src/types/objectBased/typesModel.ts +23 -6
  63. package/src/types/objectBased/typesObject.ts +11 -15
  64. package/src/types/objectBased/typesRecord.ts +11 -15
  65. package/src/types/objectBased/typesRef.ts +23 -4
  66. package/src/types/schemas.ts +59 -71
  67. package/src/types/tProp.ts +29 -29
  68. package/src/types/utility/typeToStoredData.ts +22 -21
  69. package/src/types/utility/typesCodecCore.ts +8 -1
  70. package/src/types/utility/typesCodecSupport.ts +16 -10
  71. package/src/types/utility/typesMap.ts +43 -51
  72. package/src/types/utility/typesOr.ts +38 -8
  73. package/src/types/utility/typesRefinement.ts +7 -8
  74. package/src/types/utility/typesSkipCheck.ts +7 -8
  75. package/src/types/utility/typesTag.ts +7 -8
  76. package/src/utils/ModelPool.ts +1 -18
  77. package/src/utils/decorators.ts +5 -5
  78. package/src/utils/index.ts +1 -20
  79. package/src/utils/types.ts +5 -0
  80. package/src/wrappers/ArraySet.ts +27 -14
  81. package/src/wrappers/ObjectMap.ts +1 -6
  82. package/src/wrappers/asMap.ts +1 -21
  83. package/src/wrappers/asSet.ts +0 -8
@@ -0,0 +1,276 @@
1
+ import { modelTypeKey } from "../model/metadata"
2
+ import {
3
+ findModelInfoForPrototype,
4
+ getModelSubclassDistance,
5
+ getModelSubclassDistanceForName,
6
+ } from "../modelShared/modelInfo"
7
+ import { isObject } from "../utils"
8
+
9
+ /**
10
+ * Model type matching pass used while unions match a value against their branches, so they can
11
+ * prefer the branches whose models are the closest to the ones named by the snapshot
12
+ * (`$modelType`):
13
+ * - `none`: not matching union branches; model types match snapshots of their own model and of
14
+ * its subclasses.
15
+ * - `exact`: model types only match snapshots of their own model.
16
+ * - `closest`: model types also match snapshots of subclasses, adding how far the subclass is
17
+ * from the model to `subclassDistance`, so unions can pick the branch with the lowest total.
18
+ *
19
+ * @internal
20
+ */
21
+ export type ModelTypeMatchingPass = "none" | "exact" | "closest"
22
+
23
+ let currentPass: ModelTypeMatchingPass = "none"
24
+ let subclassDistance = 0
25
+ let rejectedModelSnapshot = false
26
+
27
+ /**
28
+ * @internal
29
+ */
30
+ export function getModelTypeMatchingPass(): ModelTypeMatchingPass {
31
+ return currentPass
32
+ }
33
+
34
+ /**
35
+ * Records that a model type matched a snapshot of a subclass `distance` levels below it.
36
+ *
37
+ * @internal
38
+ */
39
+ export function addModelSubclassDistance(distance: number): void {
40
+ if (currentPass === "closest") {
41
+ subclassDistance += distance
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Records, while unions match their branches, that a model type (or ref) rejected a model
47
+ * snapshot that some other branch could accept: one of a subclass in the exact pass (it would
48
+ * match it in the closest pass), or one of another registered model.
49
+ *
50
+ * @internal
51
+ */
52
+ export function rejectModelSnapshot(): void {
53
+ if (currentPass !== "none") {
54
+ rejectedModelSnapshot = true
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Runs a value match, returning whether it matched, or `undefined` if it did not because a model
60
+ * type (or ref) rejected a model snapshot (see `rejectModelSnapshot`).
61
+ *
62
+ * @internal
63
+ */
64
+ export function matchUnlessModelSnapshotRejected<A, B>(
65
+ fn: (a: A, b: B) => unknown,
66
+ a: A,
67
+ b: B
68
+ ): boolean | undefined {
69
+ const prevRejectedModelSnapshot = rejectedModelSnapshot
70
+ rejectedModelSnapshot = false
71
+ try {
72
+ const matched = !!fn(a, b)
73
+ return matched || !rejectedModelSnapshot ? matched : undefined
74
+ } finally {
75
+ // let outer matches know too
76
+ rejectedModelSnapshot ||= prevRejectedModelSnapshot
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Whether a snapshot `$modelType` matches a model (`modelName`): its own model or, outside the
82
+ * exact pass, a subclass of it. Registered names are compared rather than classes, since with
83
+ * hot reloading a subclass might still extend a previous registration of the model.
84
+ *
85
+ * @internal
86
+ */
87
+ export function snapshotModelTypeMatches(snModelType: unknown, modelName: string): boolean {
88
+ if (typeof snModelType !== "string") {
89
+ return false
90
+ }
91
+ if (snModelType === modelName) {
92
+ return true
93
+ }
94
+ const distance = getModelSubclassDistanceForName(snModelType, modelName)
95
+ if (distance === undefined) {
96
+ return false
97
+ }
98
+ if (distance < 0 || currentPass === "exact") {
99
+ rejectModelSnapshot()
100
+ return false
101
+ }
102
+ addModelSubclassDistance(distance)
103
+ return true
104
+ }
105
+
106
+ /**
107
+ * Whether a snapshot `$modelType` names a registered model that is neither a model
108
+ * (`modelName`) nor a subclass of it, so processing the snapshot as that model would silently
109
+ * create an unrelated model (unregistered names are reported later by the model snapshotter).
110
+ *
111
+ * @internal
112
+ */
113
+ export function isUnrelatedSnapshotModelType(snModelType: unknown, modelName: string): boolean {
114
+ return (
115
+ typeof snModelType === "string" &&
116
+ snModelType !== modelName &&
117
+ (getModelSubclassDistanceForName(snModelType, modelName) ?? 0) < 0
118
+ )
119
+ }
120
+
121
+ /**
122
+ * Whether a model instance is of a model (`modelName`) or of a subclass of it, comparing
123
+ * registered names, since with hot reloading the instance model might still extend a previous
124
+ * registration of the model.
125
+ *
126
+ * @internal
127
+ */
128
+ export function isModelInstanceOfModelName(value: object, modelName: string): boolean {
129
+ const valueModelInfo = findModelInfoForPrototype(Object.getPrototypeOf(value))
130
+ return !!valueModelInfo && getModelSubclassDistance(valueModelInfo, modelName) >= 0
131
+ }
132
+
133
+ /**
134
+ * Whether a value is a snapshot that names its model (`$modelType`).
135
+ *
136
+ * @internal
137
+ */
138
+ export function hasSnapshotModelType(value: unknown): boolean {
139
+ return isObject(value) && typeof value[modelTypeKey] === "string"
140
+ }
141
+
142
+ /**
143
+ * Runs a function in the given model type matching pass, restoring the current one afterwards.
144
+ *
145
+ * @internal
146
+ */
147
+ export function runInModelTypeMatchingPass<A, B, R>(
148
+ pass: ModelTypeMatchingPass,
149
+ fn: (a: A, b: B) => R,
150
+ a: A,
151
+ b: B
152
+ ): R {
153
+ if (pass === "none" && currentPass === "none") {
154
+ return fn(a, b)
155
+ }
156
+
157
+ const prevPass = currentPass
158
+ const prevDistance = subclassDistance
159
+ const prevRejectedModelSnapshot = rejectedModelSnapshot
160
+ currentPass = pass
161
+ subclassDistance = 0
162
+ rejectedModelSnapshot = false
163
+ try {
164
+ return fn(a, b)
165
+ } finally {
166
+ currentPass = prevPass
167
+ subclassDistance = prevDistance
168
+ rejectedModelSnapshot = prevRejectedModelSnapshot
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Finds the union branch (candidate) that matches a value, preferring branches that match the
174
+ * exact models named by the snapshot (branches without models count as exact matches), then
175
+ * the branches whose models are the closest base models of the snapshot ones (the fewest
176
+ * subclass levels away in total), and the first one on ties.
177
+ *
178
+ * @internal
179
+ */
180
+ export function findClosestModelTypeMatch<T, R>(
181
+ candidates: ReadonlyArray<T>,
182
+ match: (candidate: T, value: unknown) => R | null | undefined,
183
+ value: unknown
184
+ ): R | undefined {
185
+ switch (currentPass) {
186
+ case "exact":
187
+ // nested inside an exact pass
188
+ return findNestedMatch(findFirstMatch, candidates, match, value, 0)
189
+ case "closest":
190
+ // nested inside a closest pass
191
+ return findNestedMatch(findClosestMatch, candidates, match, value, 0)
192
+ default:
193
+ try {
194
+ currentPass = "exact"
195
+ const exactMatch = findFirstMatch(candidates, match, value)
196
+ if (exactMatch != null) {
197
+ return exactMatch
198
+ }
199
+
200
+ currentPass = "closest"
201
+ subclassDistance = 0
202
+ // no branch matched exactly, so none can be less than one subclass level away
203
+ return findClosestMatch(candidates, match, value, 1)
204
+ } finally {
205
+ currentPass = "none"
206
+ subclassDistance = 0
207
+ rejectedModelSnapshot = false
208
+ }
209
+ }
210
+ }
211
+
212
+ function findNestedMatch<T, R>(
213
+ find: (
214
+ candidates: ReadonlyArray<T>,
215
+ match: (candidate: T, value: unknown) => R | null | undefined,
216
+ value: unknown,
217
+ minPossibleDistance: number
218
+ ) => R | undefined,
219
+ candidates: ReadonlyArray<T>,
220
+ match: (candidate: T, value: unknown) => R | null | undefined,
221
+ value: unknown,
222
+ minPossibleDistance: number
223
+ ): R | undefined {
224
+ const prevRejectedModelSnapshot = rejectedModelSnapshot
225
+ const result = find(candidates, match, value, minPossibleDistance)
226
+ if (result != null) {
227
+ // model snapshots rejected by other branches do not matter then
228
+ rejectedModelSnapshot = prevRejectedModelSnapshot
229
+ }
230
+ return result
231
+ }
232
+
233
+ /**
234
+ * Finds the first candidate that matches a value.
235
+ *
236
+ * @internal
237
+ */
238
+ export function findFirstMatch<T, R>(
239
+ candidates: ReadonlyArray<T>,
240
+ match: (candidate: T, value: unknown) => R | null | undefined,
241
+ value: unknown
242
+ ): R | undefined {
243
+ for (let i = 0; i < candidates.length; i++) {
244
+ const result = match(candidates[i], value)
245
+ if (result != null) {
246
+ return result
247
+ }
248
+ }
249
+ return undefined
250
+ }
251
+
252
+ function findClosestMatch<T, R>(
253
+ candidates: ReadonlyArray<T>,
254
+ match: (candidate: T, value: unknown) => R | null | undefined,
255
+ value: unknown,
256
+ minPossibleDistance: number
257
+ ): R | undefined {
258
+ const prevDistance = subclassDistance
259
+ let best: R | undefined
260
+ let bestDistance = Number.POSITIVE_INFINITY
261
+
262
+ for (let i = 0; i < candidates.length; i++) {
263
+ subclassDistance = 0
264
+ const result = match(candidates[i], value)
265
+ if (result != null && subclassDistance < bestDistance) {
266
+ best = result
267
+ bestDistance = subclassDistance
268
+ if (bestDistance <= minPossibleDistance) {
269
+ break
270
+ }
271
+ }
272
+ }
273
+
274
+ subclassDistance = prevDistance + (best != null ? bestDistance : 0)
275
+ return best
276
+ }
@@ -90,6 +90,11 @@ export function typesDataModelData<M = never, K = M>(
90
90
 
91
91
  const resolvedDataTypeChecker = resolveTypeChecker(standardDataType)
92
92
 
93
+ const processorPlan = snapshotProcessorPlan(
94
+ () => [resolvedDataTypeChecker],
95
+ ([processor]) => processor
96
+ )
97
+
93
98
  const thisTc: TypeChecker = new TypeChecker(
94
99
  TypeCheckerBaseType.Object,
95
100
 
@@ -104,15 +109,9 @@ export function typesDataModelData<M = never, K = M>(
104
109
  return resolvedDataTypeChecker.snapshotType(value) ? thisTc : null
105
110
  },
106
111
 
107
- snapshotProcessorPlan(
108
- () => [resolvedDataTypeChecker],
109
- ([processor]) => processor
110
- ),
112
+ processorPlan,
111
113
 
112
- snapshotProcessorPlan(
113
- () => [resolvedDataTypeChecker],
114
- ([processor]) => processor
115
- )
114
+ processorPlan
116
115
  )
117
116
 
118
117
  return thisTc
@@ -2,14 +2,20 @@ import type { O } from "ts-toolbelt"
2
2
  import type { AnyModel } from "../../model/BaseModel"
3
3
  import { getModelMetadata } from "../../model/getModelMetadata"
4
4
  import { modelTypeKey } from "../../model/metadata"
5
- import { isModelClass } from "../../model/utils"
5
+ import { isModel, isModelClass } from "../../model/utils"
6
6
  import type { ModelClass } from "../../modelShared/BaseModelShared"
7
7
  import { modelInfoByClass } from "../../modelShared/modelInfo"
8
8
  import { getInternalModelClassPropsInfo } from "../../modelShared/modelPropsInfo"
9
9
  import { noDefaultValue } from "../../modelShared/prop"
10
10
  import { isObject, lazy, setProtoProp } from "../../utils"
11
11
  import { getTypeInfo } from "../getTypeInfo"
12
+ import {
13
+ isModelInstanceOfModelName,
14
+ isUnrelatedSnapshotModelType,
15
+ snapshotModelTypeMatches,
16
+ } from "../modelTypeMatching"
12
17
  import { registerStandardTypeResolver, resolveTypeChecker } from "../resolveTypeChecker"
18
+ import { SnapshotTypeMismatchError } from "../SnapshotTypeMismatchError"
13
19
  import type { AnyStandardType, ModelType } from "../schemas"
14
20
  import { TypeCheckError } from "../TypeCheckError"
15
21
  import {
@@ -73,7 +79,10 @@ export function typesModel<M = never, K = M>(modelClass: _ClassOrObject<M, K>):
73
79
  TypeCheckerBaseType.Object,
74
80
 
75
81
  (value, path, typeCheckedValue) => {
76
- if (!(value instanceof modelClazz)) {
82
+ if (
83
+ !(value instanceof modelClazz) &&
84
+ !(isModel(value) && isModelInstanceOfModelName(value, modelInfo.name))
85
+ ) {
77
86
  return new TypeCheckError({
78
87
  path,
79
88
  expectedTypeName: typeName,
@@ -96,9 +105,9 @@ export function typesModel<M = never, K = M>(modelClass: _ClassOrObject<M, K>):
96
105
  return null
97
106
  }
98
107
 
99
- if (value[modelTypeKey] !== undefined) {
100
- // fast check
101
- return value[modelTypeKey] === modelInfo.name ? thisTc : null
108
+ const snModelType = value[modelTypeKey]
109
+ if (snModelType !== undefined) {
110
+ return snapshotModelTypeMatches(snModelType, modelInfo.name) ? thisTc : null
102
111
  }
103
112
 
104
113
  if (resolvedDataTypeChecker) {
@@ -112,7 +121,15 @@ export function typesModel<M = never, K = M>(modelClass: _ClassOrObject<M, K>):
112
121
  // No withErrorPathSegment wrapping needed here — model snapshot processors
113
122
  // delegate to the data type checker, which handles path segments at the property level.
114
123
  (sn) => {
115
- if (sn[modelTypeKey]) {
124
+ const snModelType = sn[modelTypeKey]
125
+ if (typeof snModelType === "string") {
126
+ // the snapshot names its own model; make sure it is this model or a subclass of it
127
+ if (isUnrelatedSnapshotModelType(snModelType, modelInfo.name)) {
128
+ throw new SnapshotTypeMismatchError({
129
+ expectedTypeName: typeName,
130
+ actualValue: sn,
131
+ })
132
+ }
116
133
  return sn
117
134
  } else {
118
135
  return {
@@ -173,6 +173,14 @@ function typesObjectHelper<S>(objFn: S, frozen: boolean, typeInfoGen: TypeInfoGe
173
173
  }
174
174
  )
175
175
 
176
+ const processorPlan = snapshotProcessorPlan(
177
+ () => schemaEntries.map((entry) => entry.getResolvedChecker()),
178
+ (processors) =>
179
+ processors.some(Boolean)
180
+ ? (obj: Record<string, unknown>) => applySnapshotProcessor(obj, processors)
181
+ : undefined
182
+ )
183
+
176
184
  const thisTc: TypeChecker = new TypeChecker(
177
185
  TypeCheckerBaseType.Object,
178
186
 
@@ -208,21 +216,9 @@ function typesObjectHelper<S>(objFn: S, frozen: boolean, typeInfoGen: TypeInfoGe
208
216
  return thisTc
209
217
  },
210
218
 
211
- snapshotProcessorPlan(
212
- () => schemaEntries.map((entry) => entry.getResolvedChecker()),
213
- (processors) =>
214
- processors.some(Boolean)
215
- ? (obj: Record<string, unknown>) => applySnapshotProcessor(obj, processors)
216
- : undefined
217
- ),
218
-
219
- snapshotProcessorPlan(
220
- () => schemaEntries.map((entry) => entry.getResolvedChecker()),
221
- (processors) =>
222
- processors.some(Boolean)
223
- ? (obj: Record<string, unknown>) => applySnapshotProcessor(obj, processors)
224
- : undefined
225
- )
219
+ processorPlan,
220
+
221
+ processorPlan
226
222
  )
227
223
 
228
224
  return thisTc
@@ -83,6 +83,14 @@ export function typesRecord<T extends AnyType>(valueType: T): RecordType<T> {
83
83
 
84
84
  const checkRecordValues = createChunkedRecordCachedCheck(iterateRecordEntries, checkRecordEntry)
85
85
 
86
+ const processorPlan = snapshotProcessorPlan(
87
+ () => [valueChecker],
88
+ ([processor]) =>
89
+ processor
90
+ ? (obj: Record<string, unknown>) => applySnapshotProcessor(obj, processor)
91
+ : undefined
92
+ )
93
+
86
94
  const thisTc: TypeChecker = new TypeChecker(
87
95
  TypeCheckerBaseType.Object,
88
96
 
@@ -126,21 +134,9 @@ export function typesRecord<T extends AnyType>(valueType: T): RecordType<T> {
126
134
  return thisTc
127
135
  },
128
136
 
129
- snapshotProcessorPlan(
130
- () => [valueChecker],
131
- ([processor]) =>
132
- processor
133
- ? (obj: Record<string, unknown>) => applySnapshotProcessor(obj, processor)
134
- : undefined
135
- ),
136
-
137
- snapshotProcessorPlan(
138
- () => [valueChecker],
139
- ([processor]) =>
140
- processor
141
- ? (obj: Record<string, unknown>) => applySnapshotProcessor(obj, processor)
142
- : undefined
143
- )
137
+ processorPlan,
138
+
139
+ processorPlan
144
140
  )
145
141
 
146
142
  return thisTc
@@ -1,9 +1,16 @@
1
1
  import { modelTypeKey } from "../../model/metadata"
2
+ import { isModel } from "../../model/utils"
2
3
  import { modelInfoByClass } from "../../modelShared/modelInfo"
3
4
  import type { Ref, RefConstructor } from "../../ref/Ref"
4
5
  import { isObject } from "../../utils"
6
+ import {
7
+ isModelInstanceOfModelName,
8
+ isUnrelatedSnapshotModelType,
9
+ snapshotModelTypeMatches,
10
+ } from "../modelTypeMatching"
5
11
  import { typesString } from "../primitiveBased/typesPrimitive"
6
12
  import { resolveTypeChecker } from "../resolveTypeChecker"
13
+ import { SnapshotTypeMismatchError } from "../SnapshotTypeMismatchError"
7
14
  import type { ModelType } from "../schemas"
8
15
  import { TypeCheckError } from "../TypeCheckError"
9
16
  import { TypeChecker, TypeCheckerBaseType, TypeInfo } from "../TypeChecker"
@@ -36,7 +43,10 @@ export function typesRef<O extends object>(refConstructor: RefConstructor<O>): M
36
43
  TypeCheckerBaseType.Object,
37
44
 
38
45
  (value, path, typeCheckedValue) => {
39
- if (!(value instanceof refConstructor.refClass)) {
46
+ if (
47
+ !(value instanceof refConstructor.refClass) &&
48
+ !(isModel(value) && isModelInstanceOfModelName(value, modelInfo.name))
49
+ ) {
40
50
  return new TypeCheckError({
41
51
  path,
42
52
  expectedTypeName: typeName,
@@ -56,16 +66,25 @@ export function typesRef<O extends object>(refConstructor: RefConstructor<O>): M
56
66
  return null
57
67
  }
58
68
 
59
- if (obj[modelTypeKey] !== undefined) {
69
+ const snModelType = obj[modelTypeKey]
70
+ if (snModelType !== undefined) {
60
71
  // fast check
61
- return obj[modelTypeKey] === modelInfo.name ? thisTc : null
72
+ return snapshotModelTypeMatches(snModelType, modelInfo.name) ? thisTc : null
62
73
  }
63
74
 
64
75
  return refDataTypeChecker.snapshotType(obj) ? thisTc : null
65
76
  },
66
77
 
67
78
  (sn: Record<string, unknown>) => {
68
- if (sn[modelTypeKey]) {
79
+ const snModelType = sn[modelTypeKey]
80
+ if (typeof snModelType === "string") {
81
+ // the snapshot names its own model; make sure it is this ref or a subclass of it
82
+ if (isUnrelatedSnapshotModelType(snModelType, modelInfo.name)) {
83
+ throw new SnapshotTypeMismatchError({
84
+ expectedTypeName: typeName,
85
+ actualValue: sn,
86
+ })
87
+ }
69
88
  return sn
70
89
  } else {
71
90
  return {