Maps, Sets, Dates, BigInt
Overview
Although mobx-keystone keeps snapshots JSON-friendly, you can still work with Map, Set, Date, and bigint values. The recommended approaches are:
- Codecs (
types.codec(...)and built-in codecs) — reusable typed schemas that convert between a runtime value and its snapshot representation. Recommended for most use cases. - Collection wrappers (
asMap/asSet) — lightweight function wrappers that present aMap/Setinterface over plain arrays/objects.
Codecs
Codecs are typed schemas where the runtime value differs from the encoded snapshot value. They are part of the types.* system and integrate seamlessly with tProp(...), typeCheck(...), fromSnapshot(...), and getSnapshot(...).
Built-in codecs
These codecs are built in:
| Codec | Runtime type | Snapshot type |
|---|---|---|
types.bigint | bigint | string |
types.dateAsTimestamp | Date | number (timestamp) |
types.dateAsIsoString | Date | string (ISO 8601) |
types.mapFromObject(valueType) | Map<string, V> | Record<string, V> |
types.mapFromArray(keyType, valueType) | Map<K, V> | Array<[K, V]> |
types.setFromArray(valueType) | Set<V> | Array<V> |
Basic example
@model("MyApp/M")
class M extends Model({
id: tProp(types.bigint),
createdAt: tProp(types.dateAsTimestamp),
totals: tProp(types.mapFromObject(types.number), () => new Map()),
tags: tProp(types.setFromArray(types.string), () => new Set<string>()),
}) {}
const m = new M({
id: 1n,
createdAt: new Date(),
totals: new Map([["a", 1]]),
tags: new Set(["x"]),
})
m.id // bigint
m.createdAt // Date
m.totals // Map<string, number>
m.tags // Set<string>
getSnapshot(m) // { id: "1", createdAt: 1234567890, totals: { a: 1 }, tags: ["x"], ... }
For codec-backed maps, getOrInsert and getOrInsertComputed return the stored runtime value, just like get. When that value is a collection, mutations through the returned value update the map's stored data.
Codec-backed sets support union, intersection, difference, and symmetricDifference with native set results and iteration order. Intersection checks membership using each collection’s own has method, including codec conversions. The boolean predicates isSubsetOf, isSupersetOf, and isDisjointFrom use each collection’s own membership rules, including codec conversions, and all seven operations work without native set-method support.
Nested / composed codecs
A types.refinement wrapped around a codec checks the decoded runtime value during model construction and writes; invalid writes are rejected and rolled back.
Codecs compose naturally with each other and with other types.* combinators:
@model("MyApp/Schedule")
class Schedule extends Model({
// Map with codec-converted keys AND values
events: tProp(
types.mapFromArray(types.dateAsIsoString, types.bigint),
() => new Map()
),
// Optional bigint
limit: tProp(types.maybe(types.bigint)),
// Array of dates
holidays: tProp(types.array(types.dateAsTimestamp), () => []),
}) {}
const s = new Schedule({
events: new Map([[new Date("2024-01-01"), 42n]]),
limit: undefined,
holidays: [new Date("2024-12-25")],
})
s.events // Map<Date, bigint>
s.limit // bigint | undefined
s.holidays // Date[]
// Snapshot is fully JSON-serializable:
// { events: [["2024-01-01T00:00:00.000Z", "42"]], limit: undefined, holidays: [1735084800000], ... }
Custom codecs
You can create your own codecs with types.codec(...):
const urlType = types.codec({
typeName: "url",
encodedType: types.string,
is(value): value is URL {
return value instanceof URL
},
transform({ originalValue, cachedTransformedValue }) {
return cachedTransformedValue ?? new URL(originalValue)
},
untransform({ transformedValue, cacheTransformedValue }) {
cacheTransformedValue()
return transformedValue.toString()
},
})
// Use it like any other type:
@model("MyApp/Link")
class Link extends Model({
href: tProp(urlType),
altUrls: tProp(types.array(urlType), () => []),
}) {}
See the Runtime Type Checking page for the full types.codec(...) API reference.
There are also types.dateTimestamp (equivalent to types.integer) and types.dateString (equivalent to types.nonEmptyString), but these are plain type checkers with no runtime Date conversion. Prefer the codec versions types.dateAsTimestamp and types.dateAsIsoString for new code.
Collection wrappers
asMap and asSet are low-level utility functions that wrap plain observable objects/arrays into Map/Set-like interfaces. They are useful when you need direct control over the underlying data. Unlike codecs, they do not convert values. They only provide a different interface over the same underlying data.
When a MobX interceptor cancels an addition, update, or deletion on the backing data, the wrapper keeps its existing contents. A canceled deletion returns false.
Collection wrappers return the same wrapper instance for the same backing object.
asMap collection wrapper
asMap wraps either an object of type { [k: string]: V } or an array of type [string, V][] and exposes it as a Map<string, V>-like interface.
If the backed property is an object, operations should be as fast as usual.
If the backed property is an array, the following operations will be slower than usual:
setoperations will need to iterate the backed array until the item to update is found.deleteoperations will need to iterate the backed array until the item to be deleted is found.
@model("MyApp/Settings")
class Settings extends Model({
flagsByName: prop<Record<string, boolean>>(() => ({})),
}) {
get flagsMap() {
return asMap(this.flagsByName)
}
@modelAction
replaceFlagsMap(next: Map<string, boolean>) {
this.flagsByName = mapToObject(next)
}
}
// `flagsMap` can now be used like a standard `Map`
@model("MyApp/Scoreboard")
class Scoreboard extends Model({
entries: prop<[string, number][]>(() => []),
}) {
get entriesMap() {
return asMap(this.entries)
}
@modelAction
replaceEntriesMap(next: Map<string, number>) {
this.entries = mapToArray(next)
}
}
// `entriesMap` can now be used like a standard `Map`
getOrInsert and getOrInsertComputed on an asMap wrapper return the stored value, including any observable conversion, consistently across supported MobX versions.
To convert it back to an object/array you can use mapToObject(map) or mapToArray(map). When the map is already a collection wrapper, these return the backing object/array rather than doing a fresh conversion.
asSet collection wrapper
asSet wraps a property of type V[] into a Set<V>-like interface:
Since the backed property is actually an array, the following operations will be slower than usual:
deleteoperations will need to iterate the backed array until it finds the value to be deleted.
@model("MyApp/Selection")
class Selection extends Model({
selectedIds: prop<string[]>(() => []),
}) {
get selectedIdSet() {
return asSet(this.selectedIds)
}
@modelAction
replaceSelectedIdSet(next: Set<string>) {
this.selectedIds = setToArray(next)
}
}
// `selectedIdSet` can now be used like a standard `Set`
To convert it back to an array you can use setToArray(set). When the set is already a collection wrapper it returns the backed array rather than doing a conversion.
Deprecated approaches
Property transforms (.withTransform(...)), the built-in transform helpers and the ObjectMap / ArraySet collection models still work but are deprecated in favor of codecs. See Property Transforms and Collection Models (deprecated) for the replacement of each.