diff --git a/src/utils/DMRoomMap.js b/src/utils/DMRoomMap.ts
similarity index 76%
rename from src/utils/DMRoomMap.js
rename to src/utils/DMRoomMap.ts
index 4e219b1611..e49b74c380 100644
--- a/src/utils/DMRoomMap.js
+++ b/src/utils/DMRoomMap.ts
@@ -1,6 +1,5 @@
 /*
-Copyright 2016 OpenMarket Ltd
-Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
+Copyright 2016, 2019, 2021 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -17,7 +16,9 @@ limitations under the License.
 
 import {MatrixClientPeg} from '../MatrixClientPeg';
 import {uniq} from "lodash";
-import {Room} from "matrix-js-sdk/src/matrix";
+import {Room} from "matrix-js-sdk/src/models/room";
+import {Event} from "matrix-js-sdk/src/models/event";
+import {MatrixClient} from "matrix-js-sdk/src/client";
 
 /**
  * Class that takes a Matrix Client and flips the m.direct map
@@ -27,29 +28,31 @@ import {Room} from "matrix-js-sdk/src/matrix";
  * With 'start', this can also keep itself up to date over time.
  */
 export default class DMRoomMap {
+    private static sharedInstance: DMRoomMap;
+
+    private matrixClient: MatrixClient;
+    // TODO: convert these to maps
+    private roomToUser: {[key: string]: string} = null;
+    private userToRooms: {[key: string]: string[]} = null;
+    private hasSentOutPatchDirectAccountDataPatch: boolean;
+    private mDirectEvent: Event;
+
     constructor(matrixClient) {
         this.matrixClient = matrixClient;
-        this.roomToUser = null;
-        // see _onAccountData
-        this._hasSentOutPatchDirectAccountDataPatch = false;
-
-        // XXX: Force-bind the event handler method because it
-        // doesn't call it with our object as the 'this'
-        // (use a static property arrow function for this when we can)
-        this._onAccountData = this._onAccountData.bind(this);
+        // see onAccountData
+        this.hasSentOutPatchDirectAccountDataPatch = false;
 
         const mDirectEvent = matrixClient.getAccountData('m.direct');
         this.mDirectEvent = mDirectEvent ? mDirectEvent.getContent() : {};
-        this.userToRooms = null;
     }
 
     /**
      * Makes and returns a new shared instance that can then be accessed
      * with shared(). This returned instance is not automatically started.
      */
-    static makeShared() {
-        DMRoomMap._sharedInstance = new DMRoomMap(MatrixClientPeg.get());
-        return DMRoomMap._sharedInstance;
+    public static makeShared(): DMRoomMap {
+        DMRoomMap.sharedInstance = new DMRoomMap(MatrixClientPeg.get());
+        return DMRoomMap.sharedInstance;
     }
 
     /**
@@ -57,32 +60,33 @@ export default class DMRoomMap {
      * that uses the singleton matrix client
      * The shared instance must be started before use.
      */
-    static shared() {
-        return DMRoomMap._sharedInstance;
+    public static shared(): DMRoomMap {
+        return DMRoomMap.sharedInstance;
     }
 
-    start() {
-        this._populateRoomToUser();
-        this.matrixClient.on("accountData", this._onAccountData);
+    public start() {
+        this.populateRoomToUser();
+        this.matrixClient.on("accountData", this.onAccountData);
     }
 
-    stop() {
-        this.matrixClient.removeListener("accountData", this._onAccountData);
+    public stop() {
+        this.matrixClient.removeListener("accountData", this.onAccountData);
     }
 
-    _onAccountData(ev) {
+    private onAccountData = (ev) => {
         if (ev.getType() == 'm.direct') {
             this.mDirectEvent = this.matrixClient.getAccountData('m.direct').getContent() || {};
             this.userToRooms = null;
             this.roomToUser = null;
         }
     }
+
     /**
      * some client bug somewhere is causing some DMs to be marked
      * with ourself, not the other user. Fix it by guessing the other user and
      * modifying userToRooms
      */
-    _patchUpSelfDMs(userToRooms) {
+    private patchUpSelfDMs(userToRooms) {
         const myUserId = this.matrixClient.getUserId();
         const selfRoomIds = userToRooms[myUserId];
         if (selfRoomIds) {
@@ -118,10 +122,10 @@ export default class DMRoomMap {
         }
     }
 
-    getDMRoomsForUserId(userId) {
+    public getDMRoomsForUserId(userId): string[] {
         // Here, we return the empty list if there are no rooms,
         // since the number of conversations you have with this user is zero.
-        return this._getUserToRooms()[userId] || [];
+        return this.getUserToRooms()[userId] || [];
     }
 
     /**
@@ -129,7 +133,7 @@ export default class DMRoomMap {
      * @param {string[]} ids The identifiers (user IDs and email addresses) to look for.
      * @returns {Room} The DM room which all IDs given share, or falsey if no common room.
      */
-    getDMRoomForIdentifiers(ids) {
+    public getDMRoomForIdentifiers(ids: string[]): Room {
         // TODO: [Canonical DMs] Handle lookups for email addresses.
         // For now we'll pretend we only get user IDs and end up returning nothing for email addresses
 
@@ -145,7 +149,7 @@ export default class DMRoomMap {
         return joinedRooms[0];
     }
 
-    getUserIdForRoomId(roomId) {
+    public getUserIdForRoomId(roomId: string) {
         if (this.roomToUser == null) {
             // we lazily populate roomToUser so you can use
             // this class just to call getDMRoomsForUserId
@@ -153,7 +157,7 @@ export default class DMRoomMap {
             // convenient wrapper and there's no point
             // iterating through the map if getUserIdForRoomId()
             // is never called.
-            this._populateRoomToUser();
+            this.populateRoomToUser();
         }
         // Here, we return undefined if the room is not in the map:
         // the room ID you gave is not a DM room for any user.
@@ -167,7 +171,7 @@ export default class DMRoomMap {
         return this.roomToUser[roomId];
     }
 
-    getUniqueRoomsWithIndividuals(): {[userId: string]: Room} {
+    public getUniqueRoomsWithIndividuals(): {[userId: string]: Room} {
         if (!this.roomToUser) return {}; // No rooms means no map.
         return Object.keys(this.roomToUser)
             .map(r => ({userId: this.getUserIdForRoomId(r), room: this.matrixClient.getRoom(r)}))
@@ -175,20 +179,20 @@ export default class DMRoomMap {
             .reduce((obj, r) => (obj[r.userId] = r.room) && obj, {});
     }
 
-    _getUserToRooms() {
+    private getUserToRooms(): {[key: string]: string[]} {
         if (!this.userToRooms) {
-            const userToRooms = this.mDirectEvent;
+            const userToRooms = this.mDirectEvent as {[key: string]: string[]};
             const myUserId = this.matrixClient.getUserId();
             const selfDMs = userToRooms[myUserId];
             if (selfDMs && selfDMs.length) {
-                const neededPatching = this._patchUpSelfDMs(userToRooms);
+                const neededPatching = this.patchUpSelfDMs(userToRooms);
                 // to avoid multiple devices fighting to correct
                 // the account data, only try to send the corrected
                 // version once.
                 console.warn(`Invalid m.direct account data detected ` +
                     `(self-chats that shouldn't be), patching it up.`);
-                if (neededPatching && !this._hasSentOutPatchDirectAccountDataPatch) {
-                    this._hasSentOutPatchDirectAccountDataPatch = true;
+                if (neededPatching && !this.hasSentOutPatchDirectAccountDataPatch) {
+                    this.hasSentOutPatchDirectAccountDataPatch = true;
                     this.matrixClient.setAccountData('m.direct', userToRooms);
                 }
             }
@@ -197,9 +201,9 @@ export default class DMRoomMap {
         return this.userToRooms;
     }
 
-    _populateRoomToUser() {
+    private populateRoomToUser() {
         this.roomToUser = {};
-        for (const user of Object.keys(this._getUserToRooms())) {
+        for (const user of Object.keys(this.getUserToRooms())) {
             for (const roomId of this.userToRooms[user]) {
                 this.roomToUser[roomId] = user;
             }
diff --git a/test/components/views/rooms/RoomList-test.js b/test/components/views/rooms/RoomList-test.js
index c2cad431f4..1c2a1c9992 100644
--- a/test/components/views/rooms/RoomList-test.js
+++ b/test/components/views/rooms/RoomList-test.js
@@ -9,8 +9,8 @@ import sdk from '../../../skinned-sdk';
 import { DragDropContext } from 'react-beautiful-dnd';
 
 import dis from '../../../../src/dispatcher/dispatcher';
-import DMRoomMap from '../../../../src/utils/DMRoomMap.js';
-import GroupStore from '../../../../src/stores/GroupStore.js';
+import DMRoomMap from '../../../../src/utils/DMRoomMap';
+import GroupStore from '../../../../src/stores/GroupStore';
 
 import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
 import {DefaultTagID} from "../../../../src/stores/room-list/models";
diff --git a/test/utils/ShieldUtils-test.js b/test/utils/ShieldUtils-test.js
index e4c0c671e3..8e3b19c1c4 100644
--- a/test/utils/ShieldUtils-test.js
+++ b/test/utils/ShieldUtils-test.js
@@ -42,7 +42,7 @@ describe("mkClient self-test", function() {
 
 describe("shieldStatusForMembership self-trust behaviour", function() {
     beforeAll(() => {
-        DMRoomMap._sharedInstance = {
+        DMRoomMap.sharedInstance = {
             getUserIdForRoomId: (roomId) => roomId === "DM" ? "@any:h" : null,
         };
     });