Skip to main content

Maps, Sets, Dates, BigInt

Overview

Although mobx-keystone doesn't support properties which are Maps/Sets/Dates/BigInt directly (for JSON compatibility purposes), you can still simulate them in three ways:

  1. The new property transforms.
  2. The ObjectMap and ArraySet collection models.
  3. The asSet and asMap collection wrappers.

The new property transforms

mobx-keystone provides out of the box these property transforms:

  • timestampToDateTransform() - Transforms between a number and a Date.
  • isoStringToDateTransform() - Transforms between a string and a Date.
  • objectToMapTransform() - Transforms between a Record<string, V> and a Map<string, V>. Note this uses asMap internally, so the same limitations described below apply.
  • arrayToMapTransform() - Transforms between a Array<[K, V]> and a Map<K, V>. Note this uses asMap internally, so the same limitations described below apply.
  • arrayToSetTransform() - Transforms between a Array<V> and a Set<V>. Note this uses asSet internally, so the same limitations described below apply.
  • stringToBigIntTransform() - Transforms between a string and a bigint.

Using a transform is as easy as calling .withTransform(transform) as part of a model property definition. For example:

@model(...)
class M extends Model({
date: prop<number>().withTransform(timestampToDateTransform()).withSetter()
}) {}

const m = new M({
date: new Date()
})

m.date // `Date`
m.setDate(new Date()) // ok
m.$.date // `number`

getSnapshot(m) // `{ date: number, ... }`

Another example:

@model(...)
class M extends Model({
map: prop<Record<string, number>>().withTransform(objectToMapTransform())
}) {}

const m = new M({
map: new Map(...)
})

m.map // `Map<string, number>`
m.$.map // `Record<string, number>`

getSnapshot(m) // `{ map: Record<string, number>, ... }`

Creating your own custom property transform

// first we designate the transform, with the type
// `ModelPropTransform<TOriginalValue, TTransformedValue>`
const _timestampToDateTransform: ModelPropTransform<number, Date> = {
transform({ originalValue, cachedTransformedValue, setOriginalValue }) {
// `originalValue` is the original (number) value to transform

// `cachedTransformedValue` is previously transformed value (`Date`) related
// to that original value (if any)

// `setOriginalValue` can be called in case we want to change the original value

return cachedTransformedValue ?? new ImmutableDate(originalValue)
},

untransform({ transformedValue, cacheTransformedValue }) {
// `transformedValue` is the transformed value (`Date`)

// `cacheTransformedValue()` can be called if we want to save into the cache
// that the current `transformedValue` can be cached and reused for that particular
// original value

if (transformedValue instanceof ImmutableDate) {
cacheTransformedValue()
}
return +transformedValue
},
}

// we need to export it as a function that returns the transform to keep TS happy
// whenever a generic is involed (e.g. see the source code of `arrayToSetTransform`)
// we will always return the same instance though instead of recreating it every time
export const timestampToDateTransform = () => _timestampToDateTransform

Collection models

ObjectMap collection model

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

All the usual map operations are available (clear, set, get, has, keys, values, ...), and the snapshot representation of this model will be something like:

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

ArraySet collection model

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

All the usual set operations are available (clear, add, has, keys, values, ...), and the snapshot representation of this model will be something like:

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

Collection wrappers

Note: Collection wrappers will return the same collection given a 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.