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"], ... }

Nested / composed codecs

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 can be 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.

Note: Collection wrappers will return the same collection given the same backed object.

asMap collection wrapper

asMap will wrap either an object of type { [k: string]: V } or an array of type [string, V][] and wrap it into a Map<string, V> alike 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.
class ... {
// given `myRecord: prop<{ [k: string]: V }>(() => ({}))`
get myMap() {
return asMap(this.myRecord)
}

// and if a setter is required
@modelAction
setMyMap(myMap: Map<string, V>) {
this.myRecord = mapToObject(myMap)
}
}

class ... {
// given `myArrayMap: prop<[string, V][]>(() => [])`
get myMap() {
return asMap(this.myArrayMap)
}

// and if a setter is required
@modelAction
setMyMap(myMap: Map<string, V>) {
this.myArrayMap = mapToArray(myMap)
}
}

// then `myMap` can be used as a standard `Map`

To convert it back to an object/array you can use mapToObject(map) or mapToArray(map). When the map is a collection wrapper it will return the backed object rather than do a conversion.

asSet collection wrapper

asSet will wrap a property of type V[] and wrap it into a Set<V> alike interface:

Note that, currently, 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.
class ... {
// given `myArraySet: prop<V[]>(() => [])`
get mySet() {
return asSet(this.myArraySet)
}

// and if a setter is required
@modelAction
setMySet(mySet: Set<V>) {
this.myArraySet = setToArray(mySet)
}
}

// then `mySet` can be used as a standard `Set`

To convert it back to an array you can use setToArray(set). When the map is a collection wrapper it will return the backed object rather than do a conversion.


Deprecated approaches

The following approaches are still functional but deprecated. For each one, the recommended alternative is listed.

Property transforms (.withTransform(...))

Alternative: Use tProp(types.codec(...)) or a built-in codec type. Wrap with types.skipCheck(...) if you don't need runtime validation.

Property transforms are applied per-property on top of a plain prop(...) declaration. They convert between the stored value and the value exposed on the model instance.

Compared to codecs, property transforms:

  • Don't integrate with the type system (no typeCheck(...), no typed fromSnapshot/getSnapshot)
  • Don't compose — each property declares its own transform
  • Don't support nested conversion (e.g. a Map<string, Date>)
  • Require you to manually declare snapshot types via prop<StoredType>()
// ❌ Deprecated
@model("MyApp/M")
class M extends Model({
date: prop<number>().withTransform(timestampToDateTransform()).withSetter(),
}) {}

// ✅ Preferred
@model("MyApp/M")
class M extends Model({
date: tProp(types.dateAsTimestamp).withSetter(),
// or without runtime checking:
// date: tProp(types.skipCheck(types.dateAsTimestamp)).withSetter(),
}) {}

Creating a custom property transform

If you have a one-off conversion that doesn't warrant a full codec, you can still create a custom property transform:

// ModelPropTransform<TOriginalValue, TTransformedValue>
const _timestampToDateTransform: ModelPropTransform<number, Date> = {
transform({ originalValue, cachedTransformedValue, setOriginalValue }) {
return cachedTransformedValue ?? new ImmutableDate(originalValue)
},
untransform({ transformedValue, cacheTransformedValue }) {
if (transformedValue instanceof ImmutableDate) {
cacheTransformedValue()
}
return +transformedValue
},
}

export const timestampToDateTransform = () => _timestampToDateTransform

However, for reusable conversions, prefer types.codec(...) — see the Custom codecs section above.

Built-in transform helpers

All built-in transform helpers are deprecated. Use the corresponding codec instead:

Deprecated transformCodec alternative
timestampToDateTransform()types.dateAsTimestamp
isoStringToDateTransform()types.dateAsIsoString
stringToBigIntTransform()types.bigint
objectToMapTransform()types.mapFromObject(valueType)
arrayToMapTransform()types.mapFromArray(keyType, valueType)
arrayToSetTransform()types.setFromArray(valueType)
tip

Codecs additionally support nested conversion. For example, types.mapFromArray(types.dateAsIsoString, types.bigint) converts both keys and values, which is not possible with arrayToMapTransform().

If you want the codec conversion without runtime type-checking overhead, wrap with types.skipCheck(...):

tProp(types.skipCheck(types.dateAsTimestamp))
// equivalent to: prop<number>().withTransform(timestampToDateTransform())
// but with proper snapshot typing and composability

Collection models (ObjectMap / ArraySet)

Alternative: Use tProp(types.mapFromObject(...)), tProp(types.mapFromArray(...)), or tProp(types.setFromArray(...)).

ObjectMap and ArraySet are special model wrappers that provide Map-like and Set-like interfaces. They produce snapshots that include $modelType and $modelId metadata, whereas codecs produce clean plain objects/arrays.

ObjectMap collection model

// ❌ Deprecated
class ... extends Model({
myNumberMap: prop(() => objectMap<number>())
// or if there's no default value
myNumberMap: prop<ObjectMap<number>>()
}) {}

// ✅ Preferred
class ... extends Model({
myNumberMap: tProp(types.mapFromObject(types.number), () => new Map())
}) {}

Snapshot representation of ObjectMap (includes model metadata):

{
$modelType: "mobx-keystone/ObjectMap",
$modelId: "Td244...",
items: {
"key1": value1,
"key2": value2,
}
}

ArraySet collection model

// ❌ Deprecated
class ... extends Model({
myNumberSet: prop(() => arraySet<number>())
// or if there's no default value
myNumberSet: prop<ArraySet<number>>()
}) {}

// ✅ Preferred
class ... extends Model({
myNumberSet: tProp(types.setFromArray(types.number), () => new Set())
}) {}

Snapshot representation of ArraySet (includes model metadata):

{
$modelType: "mobx-keystone/ArraySet",
$modelId: "Td244...",
items: [
value1,
value2
]
}