Skip to main content

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:

  1. 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.
  2. Collection wrappers (asMap / asSet) — lightweight function wrappers that present a Map/Set interface 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:

CodecRuntime typeSnapshot type
types.bigintbigintstring
types.dateAsTimestampDatenumber (timestamp)
types.dateAsIsoStringDatestring (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.

note

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.

note

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:

  • set operations will need to iterate the backed array until the item to update is found.
  • delete operations 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:

  • delete operations 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.