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:
- The new property transforms.
- The
ObjectMap
andArraySet
collection models. - The
asSet
andasMap
collection wrappers.
The new property transforms
mobx-keystone
provides out of the box these property transforms:
timestampToDateTransform()
- Transforms between anumber
and aDate
.isoStringToDateTransform()
- Transforms between astring
and aDate
.objectToMapTransform()
- Transforms between aRecord<string, V>
and aMap<string, V>
. Note this usesasMap
internally, so the same limitations described below apply.arrayToMapTransform()
- Transforms between aArray<[K, V]>
and aMap<K, V>
. Note this usesasMap
internally, so the same limitations described below apply.arrayToSetTransform()
- Transforms between aArray<V>
and aSet<V>
. Note this usesasSet
internally, so the same limitations described below apply.stringToBigIntTransform()
- Transforms between astring
and abigint
.
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.