Improve types for get(), set(), and MapType

This commit is contained in:
Mel Bourgeois 2023-05-24 01:53:11 -05:00
parent 5db1eed181
commit e9a8af86e5
No known key found for this signature in database
GPG Key ID: 290FCF081AEDB3EC
3 changed files with 47 additions and 20 deletions

View File

@ -21,12 +21,25 @@ import * as iterator from 'lib0/iterator'
/** /**
* @template T * @template T
* @typedef {Extract<keyof T, string>} StringKey<T>
*/
/**
* This works around some weird JSDoc+TS circular reference issues: https://github.com/microsoft/TypeScript/issues/46369
* @typedef {boolean|null|string|number|Uint8Array|JsonArray|JsonObject} Json
* @typedef {Json[]} JsonArray
* @typedef {{ [key: string]: Json }} JsonObject
* @typedef {Json|AbstractType<any>} MapValue
*/
/**
* @template {Record<string, MapValue>} T
* @extends YEvent<YMap<T>> * @extends YEvent<YMap<T>>
* Event that describes the changes on a YMap. * Event that describes the changes on a YMap.
*/ */
export class YMapEvent extends YEvent { export class YMapEvent extends YEvent {
/** /**
* @param {YMap<T>} ymap The YArray that changed. * @param {YMap<T>} ymap The YMap that changed.
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {Set<any>} subs The keys that changed. * @param {Set<any>} subs The keys that changed.
*/ */
@ -37,7 +50,7 @@ export class YMapEvent extends YEvent {
} }
/** /**
* @template MapType * @template {Record<string, MapValue>} MapType
* A shared Map implementation. * A shared Map implementation.
* *
* @extends AbstractType<YMapEvent<MapType>> * @extends AbstractType<YMapEvent<MapType>>
@ -76,7 +89,8 @@ export class YMap extends AbstractType {
_integrate (y, item) { _integrate (y, item) {
super._integrate(y, item) super._integrate(y, item)
;/** @type {Map<string, any>} */ (this._prelimContent).forEach((value, key) => { ;/** @type {Map<string, any>} */ (this._prelimContent).forEach((value, key) => {
this.set(key, value) // TODO: fix
this.set(/** @type {any} */ (key), value)
}) })
this._prelimContent = null this._prelimContent = null
} }
@ -97,7 +111,8 @@ export class YMap extends AbstractType {
*/ */
const map = new YMap() const map = new YMap()
this.forEach((value, key) => { this.forEach((value, key) => {
map.set(key, value instanceof AbstractType ? /** @type {typeof value} */ (value.clone()) : value) // TODO: fix
map.set(key, /** @type {any} */ (value instanceof AbstractType ? /** @type {typeof value} */ (value.clone()) : value))
}) })
return map return map
} }
@ -170,12 +185,12 @@ export class YMap extends AbstractType {
/** /**
* Executes a provided function on once on every key-value pair. * Executes a provided function on once on every key-value pair.
* *
* @param {function(MapType,string,YMap<MapType>):void} f A function to execute on every element of this YArray. * @param {function(MapType[keyof MapType],StringKey<MapType>,YMap<MapType>):void} f A function to execute on every element of this YArray.
*/ */
forEach (f) { forEach (f) {
this._map.forEach((item, key) => { this._map.forEach((item, key) => {
if (!item.deleted) { if (!item.deleted) {
f(item.content.getContent()[item.length - 1], key, this) f(item.content.getContent()[item.length - 1], /** @type {StringKey<MapType>} */ (key), this)
} }
}) })
} }
@ -192,6 +207,7 @@ export class YMap extends AbstractType {
/** /**
* Remove a specified element from this YMap. * Remove a specified element from this YMap.
* *
* TODO: type to only allow deletion of optional elements in MapType
* @param {string} key The key of the element to remove. * @param {string} key The key of the element to remove.
*/ */
delete (key) { delete (key) {
@ -205,17 +221,18 @@ export class YMap extends AbstractType {
} }
/** /**
* @template {StringKey<MapType>} Key
* @template {MapType[Key]} Value
* Adds or updates an element with a specified key and value. * Adds or updates an element with a specified key and value.
* @template {MapType} VAL
* *
* @param {string} key The key of the element to add to this YMap * @param {Key} key The key of the element to add to this YMap
* @param {VAL} value The value of the element to add * @param {Value} value The value of the element to add
* @return {VAL} * @return {Value}
*/ */
set (key, value) { set (key, value) {
if (this.doc !== null) { if (this.doc !== null) {
transact(this.doc, transaction => { transact(this.doc, transaction => {
typeMapSet(transaction, this, key, /** @type {any} */ (value)) typeMapSet(transaction, this, key, value)
}) })
} else { } else {
/** @type {Map<string, any>} */ (this._prelimContent).set(key, value) /** @type {Map<string, any>} */ (this._prelimContent).set(key, value)
@ -224,13 +241,14 @@ export class YMap extends AbstractType {
} }
/** /**
* @template {StringKey<MapType>} Key
* Returns a specified element from this YMap. * Returns a specified element from this YMap.
* *
* @param {string} key * @param {Key} key
* @return {MapType|undefined} * @return {MapType[Key]|undefined}
*/ */
get (key) { get (key) {
return /** @type {any} */ (typeMapGet(this, key)) return /** @type {MapType[Key]|undefined} */ (typeMapGet(this, key))
} }
/** /**
@ -273,3 +291,9 @@ export class YMap extends AbstractType {
* @function * @function
*/ */
export const readYMap = _decoder => new YMap() export const readYMap = _decoder => new YMap()
/** @type {YMap<{ foo: { hello: [3, 4, { hiThere: null }] }; }>} */
const foo = new YMap();
foo.has("")

View File

@ -329,6 +329,7 @@ export const testUndoUntilChangePerformed = _tc => {
const yMap = new Y.Map() const yMap = new Y.Map()
yMap.set('hello', 'world') yMap.set('hello', 'world')
yArray.push([yMap]) yArray.push([yMap])
/** @type {Y.Map<{ key: string }>} */
const yMap2 = new Y.Map() const yMap2 = new Y.Map()
yMap2.set('key', 'value') yMap2.set('key', 'value')
yArray.push([yMap2]) yArray.push([yMap2])
@ -342,7 +343,7 @@ export const testUndoUntilChangePerformed = _tc => {
Y.transact(doc2, () => yArray2.delete(0), doc2.clientID) Y.transact(doc2, () => yArray2.delete(0), doc2.clientID)
undoManager2.undo() undoManager2.undo()
undoManager.undo() undoManager.undo()
t.compareStrings(yMap2.get('key'), 'value') t.compareStrings(yMap2.get('key') || '', 'value')
} }
/** /**

View File

@ -19,14 +19,16 @@ export const testMapHavingIterableAsConstructorParamTests = tc => {
t.assert(m1.get('number') === 1) t.assert(m1.get('number') === 1)
t.assert(m1.get('string') === 'hello') t.assert(m1.get('string') === 'hello')
/** @type {Y.Map<{ object: { x: number; }; boolean: boolean; }>} */
const m2 = new Y.Map([ const m2 = new Y.Map([
['object', { x: 1 }], ['object', { x: 1 }],
['boolean', true] ['boolean', true]
]) ])
map0.set('m2', m2) map0.set('m2', m2)
t.assert(m2.get('object').x === 1) t.assert(m2.get('object')?.x === 1)
t.assert(m2.get('boolean') === true) t.assert(m2.get('boolean') === true)
/** @type {Y.Map<any>} */
const m3 = new Y.Map([...m1, ...m2]) const m3 = new Y.Map([...m1, ...m2])
map0.set('m3', m3) map0.set('m3', m3)
t.assert(m3.get('number') === 1) t.assert(m3.get('number') === 1)
@ -281,7 +283,7 @@ export const testGetAndSetAndDeleteOfMapPropertyWithThreeConflicts = tc => {
*/ */
export const testObserveDeepProperties = tc => { export const testObserveDeepProperties = tc => {
const { testConnector, users, map1, map2, map3 } = init(tc, { users: 4 }) const { testConnector, users, map1, map2, map3 } = init(tc, { users: 4 })
const _map1 = map1.set('map', new Y.Map()) const _map1 = map1.set('map', /** @type {Y.Map<{ deepmap: Y.Map<any> }>} */ (new Y.Map()))
let calls = 0 let calls = 0
let dmapid let dmapid
map1.observeDeep(events => { map1.observeDeep(events => {
@ -306,10 +308,10 @@ export const testObserveDeepProperties = tc => {
const dmap2 = _map2.get('deepmap') const dmap2 = _map2.get('deepmap')
const dmap3 = _map3.get('deepmap') const dmap3 = _map3.get('deepmap')
t.assert(calls > 0) t.assert(calls > 0)
t.assert(compareIDs(dmap1._item.id, dmap2._item.id)) t.assert(compareIDs(dmap1?._item?.id || null, dmap2._item.id))
t.assert(compareIDs(dmap1._item.id, dmap3._item.id)) t.assert(compareIDs(dmap1?._item?.id || null, dmap3._item.id))
// @ts-ignore we want the possibility of dmapid being undefined // @ts-ignore we want the possibility of dmapid being undefined
t.assert(compareIDs(dmap1._item.id, dmapid)) t.assert(compareIDs(dmap1?._item?.id || null, dmapid))
compare(users) compare(users)
} }