diff --git a/src/utils/AbstractConnector.js b/src/utils/AbstractConnector.js
index 5f544684..f5c0566a 100644
--- a/src/utils/AbstractConnector.js
+++ b/src/utils/AbstractConnector.js
@@ -1,4 +1,4 @@
-import { Observable } from 'lib0/observable'
+import { ObservableV2 } from 'lib0/observable'
 
 import {
   Doc // eslint-disable-line
@@ -10,9 +10,9 @@ import {
  * @note This interface is experimental and it is not advised to actually inherit this class.
  *       It just serves as typing information.
  *
- * @extends {Observable<any>}
+ * @extends {ObservableV2<any>}
  */
-export class AbstractConnector extends Observable {
+export class AbstractConnector extends ObservableV2 {
   /**
    * @param {Doc} ydoc
    * @param {any} awareness
diff --git a/src/utils/Doc.js b/src/utils/Doc.js
index 0d8d471b..8a8936b9 100644
--- a/src/utils/Doc.js
+++ b/src/utils/Doc.js
@@ -14,7 +14,7 @@ import {
   ContentDoc, Item, Transaction, YEvent // eslint-disable-line
 } from '../internals.js'
 
-import { Observable } from 'lib0/observable'
+import { ObservableV2 } from 'lib0/observable'
 import * as random from 'lib0/random'
 import * as map from 'lib0/map'
 import * as array from 'lib0/array'
@@ -34,10 +34,26 @@ export const generateNewClientId = random.uint32
  */
 
 /**
- * A Yjs instance handles the state of shared data.
- * @extends Observable<string>
+ * @typedef {Object} DocEvents
+ * @property {function(Doc):void} DocEvents.destroy
+ * @property {function(Doc):void} DocEvents.load
+ * @property {function(boolean, Doc):void} DocEvents.sync
+ * @property {function(Uint8Array, any, Doc, Transaction):void} DocEvents.update
+ * @property {function(Uint8Array, any, Doc, Transaction):void} DocEvents.updateV2
+ * @property {function(Doc):void} DocEvents.beforeAllTransactions
+ * @property {function(Transaction, Doc):void} DocEvents.beforeTransaction
+ * @property {function(Transaction, Doc):void} DocEvents.beforeObserverCalls
+ * @property {function(Transaction, Doc):void} DocEvents.afterTransaction
+ * @property {function(Transaction, Doc):void} DocEvents.afterTransactionCleanup
+ * @property {function(Doc, Array<Transaction>):void} DocEvents.afterAllTransactions
+ * @property {function({ loaded: Set<Doc>, added: Set<Doc>, removed: Set<Doc> }, Doc, Transaction):void} DocEvents.subdocs
  */
-export class Doc extends Observable {
+
+/**
+ * A Yjs instance handles the state of shared data.
+ * @extends ObservableV2<DocEvents>
+ */
+export class Doc extends ObservableV2 {
   /**
    * @param {DocOpts} opts configuration
    */
@@ -115,7 +131,7 @@ export class Doc extends Observable {
       }
       this.isSynced = isSynced === undefined || isSynced === true
       if (this.isSynced && !this.isLoaded) {
-        this.emit('load', [])
+        this.emit('load', [this])
       }
     })
     /**
@@ -321,24 +337,9 @@ export class Doc extends Observable {
         transaction.subdocsRemoved.add(this)
       }, null, true)
     }
-    this.emit('destroyed', [true])
+    // @ts-ignore
+    this.emit('destroyed', [true]) // DEPRECATED!
     this.emit('destroy', [this])
     super.destroy()
   }
-
-  /**
-   * @param {string} eventName
-   * @param {function(...any):any} f
-   */
-  on (eventName, f) {
-    super.on(eventName, f)
-  }
-
-  /**
-   * @param {string} eventName
-   * @param {function} f
-   */
-  off (eventName, f) {
-    super.off(eventName, f)
-  }
 }
diff --git a/src/utils/UndoManager.js b/src/utils/UndoManager.js
index 16ccb7ae..27023ecf 100644
--- a/src/utils/UndoManager.js
+++ b/src/utils/UndoManager.js
@@ -10,13 +10,13 @@ import {
   getItemCleanStart,
   isDeleted,
   addToDeleteSet,
-  Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line
+  YEvent, Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line
 } from '../internals.js'
 
 import * as time from 'lib0/time'
 import * as array from 'lib0/array'
 import * as logging from 'lib0/logging'
-import { Observable } from 'lib0/observable'
+import { ObservableV2 } from 'lib0/observable'
 
 export class StackItem {
   /**
@@ -48,7 +48,7 @@ const clearUndoManagerStackItem = (tr, um, stackItem) => {
 /**
  * @param {UndoManager} undoManager
  * @param {Array<StackItem>} stack
- * @param {string} eventType
+ * @param {'undo'|'redo'} eventType
  * @return {StackItem?}
  */
 const popStackItem = (undoManager, stack, eventType) => {
@@ -120,7 +120,7 @@ const popStackItem = (undoManager, stack, eventType) => {
   }, undoManager)
   if (undoManager.currStackItem != null) {
     const changedParentTypes = _tr.changedParentTypes
-    undoManager.emit('stack-item-popped', [{ stackItem: undoManager.currStackItem, type: eventType, changedParentTypes }, undoManager])
+    undoManager.emit('stack-item-popped', [{ stackItem: undoManager.currStackItem, type: eventType, changedParentTypes, origin: undoManager }, undoManager])
     undoManager.currStackItem = null
   }
   return undoManager.currStackItem
@@ -139,6 +139,14 @@ const popStackItem = (undoManager, stack, eventType) => {
  * @property {Doc} [doc] The document that this UndoManager operates on. Only needed if typeScope is empty.
  */
 
+/**
+ * @typedef {Object} StackItemEvent
+ * @property {StackItem} StackItemEvent.stackItem
+ * @property {any} StackItemEvent.origin
+ * @property {'undo'|'redo'} StackItemEvent.type
+ * @property {Map<AbstractType<YEvent<any>>,Array<YEvent<any>>>} StackItemEvent.changedParentTypes
+ */
+
 /**
  * Fires 'stack-item-added' event when a stack item was added to either the undo- or
  * the redo-stack. You may store additional stack information via the
@@ -146,9 +154,9 @@ const popStackItem = (undoManager, stack, eventType) => {
  * Fires 'stack-item-popped' event when a stack item was popped from either the
  * undo- or the redo-stack. You may restore the saved stack information from `event.stackItem.meta`.
  *
- * @extends {Observable<'stack-item-added'|'stack-item-popped'|'stack-cleared'|'stack-item-updated'>}
+ * @extends {ObservableV2<{'stack-item-added':function(StackItemEvent, UndoManager):void, 'stack-item-popped': function(StackItemEvent, UndoManager):void, 'stack-cleared': function({ undoStackCleared: boolean, redoStackCleared: boolean }):void, 'stack-item-updated': function(StackItemEvent, UndoManager):void }>}
  */
-export class UndoManager extends Observable {
+export class UndoManager extends ObservableV2 {
   /**
    * @param {AbstractType<any>|Array<AbstractType<any>>} typeScope Accepts either a single type, or an array of types
    * @param {UndoManagerOptions} options
@@ -246,6 +254,9 @@ export class UndoManager extends Observable {
           keepItem(item, true)
         }
       })
+      /**
+       * @type {[StackItemEvent, UndoManager]}
+       */
       const changeEvent = [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo', changedParentTypes: transaction.changedParentTypes }, this]
       if (didAdd) {
         this.emit('stack-item-added', changeEvent)
diff --git a/tests/testHelper.js b/tests/testHelper.js
index c74b7438..56983679 100644
--- a/tests/testHelper.js
+++ b/tests/testHelper.js
@@ -34,7 +34,7 @@ export const encV1 = {
   mergeUpdates: Y.mergeUpdates,
   applyUpdate: Y.applyUpdate,
   logUpdate: Y.logUpdate,
-  updateEventName: 'update',
+  updateEventName: /** @type {'update'} */ ('update'),
   diffUpdate: Y.diffUpdate
 }
 
@@ -43,7 +43,7 @@ export const encV2 = {
   mergeUpdates: Y.mergeUpdatesV2,
   applyUpdate: Y.applyUpdateV2,
   logUpdate: Y.logUpdateV2,
-  updateEventName: 'updateV2',
+  updateEventName: /** @type {'updateV2'} */ ('updateV2'),
   diffUpdate: Y.diffUpdateV2
 }
 
diff --git a/tests/updates.tests.js b/tests/updates.tests.js
index 4ba3bab6..f3169cfe 100644
--- a/tests/updates.tests.js
+++ b/tests/updates.tests.js
@@ -15,7 +15,7 @@ import * as object from 'lib0/object'
  * @property {function(Uint8Array):{from:Map<number,number>,to:Map<number,number>}} Enc.parseUpdateMeta
  * @property {function(Y.Doc):Uint8Array} Enc.encodeStateVector
  * @property {function(Uint8Array):Uint8Array} Enc.encodeStateVectorFromUpdate
- * @property {string} Enc.updateEventName
+ * @property {'update'|'updateV2'} Enc.updateEventName
  * @property {string} Enc.description
  * @property {function(Uint8Array, Uint8Array):Uint8Array} Enc.diffUpdate
  */
@@ -169,7 +169,7 @@ const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
   // t.info('Target State: ')
   // enc.logUpdate(targetState)
 
-  cases.forEach((mergedUpdates, i) => {
+  cases.forEach((mergedUpdates) => {
     // t.info('State Case $' + i + ':')
     // enc.logUpdate(updates)
     const merged = new Y.Doc({ gc: false })
@@ -218,10 +218,10 @@ const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
 }
 
 /**
- * @param {t.TestCase} tc
+ * @param {t.TestCase} _tc
  */
-export const testMergeUpdates1 = tc => {
-  encoders.forEach((enc, i) => {
+export const testMergeUpdates1 = _tc => {
+  encoders.forEach((enc) => {
     t.info(`Using encoder: ${enc.description}`)
     const ydoc = new Y.Doc({ gc: false })
     const updates = /** @type {Array<Uint8Array>} */ ([])
@@ -299,16 +299,16 @@ export const testMergePendingUpdates = tc => {
   Y.applyUpdate(yDoc5, update4)
   Y.applyUpdate(yDoc5, serverUpdates[4])
   // @ts-ignore
-  const update5 = Y.encodeStateAsUpdate(yDoc5) // eslint-disable-line
+  const _update5 = Y.encodeStateAsUpdate(yDoc5) // eslint-disable-line
 
   const yText5 = yDoc5.getText('textBlock')
   t.compareStrings(yText5.toString(), 'nenor')
 }
 
 /**
- * @param {t.TestCase} tc
+ * @param {t.TestCase} _tc
  */
-export const testObfuscateUpdates = tc => {
+export const testObfuscateUpdates = _tc => {
   const ydoc = new Y.Doc()
   const ytext = ydoc.getText('text')
   const ymap = ydoc.getMap('map')