From 503df6219103ae99eb34465a1827e5e4a469a0b5 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 13 Mar 2023 15:07:20 +0000
Subject: [PATCH] Conform more of the codebase to `strictNullChecks` (#10358

* Conform more of the codebase to `strictNullChecks`

* Fix types

* Iterate

* Iterate
---
 src/components/views/auth/EmailField.tsx      |  2 +-
 .../views/auth/PassphraseConfirmField.tsx     |  2 +-
 src/components/views/auth/PassphraseField.tsx |  2 +-
 .../context_menus/MessageContextMenu.tsx      | 50 +++++++++----------
 .../dialogs/devtools/SettingExplorer.tsx      |  2 +-
 .../views/directory/NetworkDropdown.tsx       |  2 +-
 .../views/elements/AccessibleButton.tsx       |  5 +-
 .../views/elements/AppPermission.tsx          |  2 -
 .../elements/DesktopCapturerSourcePicker.tsx  |  3 +-
 .../views/elements/DialogButtons.tsx          |  8 +--
 src/components/views/elements/Dropdown.tsx    | 16 +++---
 .../views/elements/EffectsOverlay.tsx         |  6 +--
 .../views/elements/ErrorBoundary.tsx          | 10 ++--
 .../views/elements/EventListSummary.tsx       |  4 +-
 src/components/views/elements/Field.tsx       |  4 +-
 .../elements/GenericEventListSummary.tsx      |  2 +-
 .../elements/IRCTimelineProfileResizer.tsx    |  4 +-
 src/components/views/elements/ImageView.tsx   | 24 ++++-----
 .../views/elements/InteractiveTooltip.tsx     | 16 +++---
 .../views/elements/LanguageDropdown.tsx       |  6 +--
 .../views/elements/LazyRenderList.tsx         |  4 +-
 .../views/elements/MiniAvatarUploader.tsx     |  3 +-
 .../views/elements/PersistentApp.tsx          |  2 +-
 .../views/elements/PollCreateDialog.tsx       |  7 ++-
 src/components/views/elements/ReplyChain.tsx  |  2 +-
 .../views/elements/RoomAliasField.tsx         |  9 ++--
 src/components/views/elements/RoomTopic.tsx   |  4 +-
 .../views/elements/SearchWarning.tsx          |  2 +-
 .../views/elements/ServerPicker.tsx           |  2 +-
 .../views/elements/SettingsFlag.tsx           |  4 +-
 .../elements/SpellCheckLanguagesDropdown.tsx  | 13 +++--
 .../views/elements/TextWithTooltip.tsx        |  2 +-
 .../views/elements/UseCaseSelection.tsx       |  2 +-
 .../views/elements/UseCaseSelectionButton.tsx |  2 +-
 src/components/views/elements/Validation.tsx  |  2 +-
 .../views/messages/DownloadActionButton.tsx   |  2 +-
 .../views/messages/EditHistoryMessage.tsx     | 21 +++-----
 .../views/messages/JumpToDatePicker.tsx       |  2 +-
 .../views/messages/LegacyCallEvent.tsx        |  2 +-
 src/components/views/messages/MBeaconBody.tsx |  6 +--
 src/components/views/messages/MImageBody.tsx  | 38 +++++++-------
 .../views/messages/MJitsiWidgetEvent.tsx      |  2 +-
 .../messages/MKeyVerificationRequest.tsx      |  2 +-
 .../views/messages/MLocationBody.tsx          | 13 +++--
 src/components/views/messages/MPollBody.tsx   |  2 +-
 .../views/messages/MStickerBody.tsx           | 12 ++---
 src/components/views/messages/MVideoBody.tsx  | 24 ++++-----
 .../views/messages/MessageActionBar.tsx       | 24 ++++-----
 .../views/messages/MessageEvent.tsx           |  8 +--
 .../views/messages/ReactionsRow.tsx           |  4 +-
 .../views/messages/ReactionsRowButton.tsx     |  4 +-
 .../views/messages/SenderProfile.tsx          |  4 +-
 src/components/views/messages/TextualBody.tsx | 41 +++++++--------
 .../views/messages/TileErrorBoundary.tsx      |  6 +--
 src/components/views/rooms/Autocomplete.tsx   |  4 +-
 .../views/rooms/BasicMessageComposer.tsx      | 18 +++----
 src/components/views/rooms/E2EIcon.tsx        | 18 ++++---
 .../views/rooms/EditMessageComposer.tsx       | 11 ++--
 src/components/views/rooms/EntityTile.tsx     | 12 ++---
 src/components/views/rooms/EventTile.tsx      | 40 +++++++--------
 src/components/views/rooms/HistoryTile.tsx    |  8 +--
 .../views/rooms/LinkPreviewGroup.tsx          |  4 +-
 .../views/rooms/LinkPreviewWidget.tsx         |  8 +--
 src/components/views/rooms/MemberList.tsx     |  6 ++-
 src/components/views/rooms/MemberTile.tsx     |  7 ++-
 .../views/rooms/MessageComposerButtons.tsx    |  8 +--
 src/components/views/rooms/NewRoomIntro.tsx   | 14 +++---
 .../views/rooms/NotificationBadge.tsx         | 10 ++--
 .../views/rooms/ReadReceiptGroup.tsx          | 10 ++--
 .../views/rooms/ReadReceiptMarker.tsx         |  2 +-
 src/components/views/rooms/RoomPreviewBar.tsx |  9 ++--
 src/components/views/rooms/RoomTile.tsx       |  2 +-
 src/settings/SettingsStore.ts                 |  2 +-
 src/utils/EditorStateTransfer.ts              |  2 +-
 src/utils/EventUtils.ts                       |  2 +-
 .../user/PreferencesUserSettingsTab-test.tsx  |  6 +--
 76 files changed, 323 insertions(+), 327 deletions(-)

diff --git a/src/components/views/auth/EmailField.tsx b/src/components/views/auth/EmailField.tsx
index 0426c08f86..849f38fea7 100644
--- a/src/components/views/auth/EmailField.tsx
+++ b/src/components/views/auth/EmailField.tsx
@@ -21,7 +21,7 @@ import { _t, _td } from "../../../languageHandler";
 import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
 import * as Email from "../../../email";
 
-interface IProps extends Omit<IInputProps, "onValidate"> {
+interface IProps extends Omit<IInputProps, "onValidate" | "element"> {
     id?: string;
     fieldRef?: RefCallback<Field> | RefObject<Field>;
     value: string;
diff --git a/src/components/views/auth/PassphraseConfirmField.tsx b/src/components/views/auth/PassphraseConfirmField.tsx
index 2411547912..b314c3f838 100644
--- a/src/components/views/auth/PassphraseConfirmField.tsx
+++ b/src/components/views/auth/PassphraseConfirmField.tsx
@@ -20,7 +20,7 @@ import Field, { IInputProps } from "../elements/Field";
 import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
 import { _t, _td } from "../../../languageHandler";
 
-interface IProps extends Omit<IInputProps, "onValidate" | "label"> {
+interface IProps extends Omit<IInputProps, "onValidate" | "label" | "element"> {
     id?: string;
     fieldRef?: RefCallback<Field> | RefObject<Field>;
     autoComplete?: string;
diff --git a/src/components/views/auth/PassphraseField.tsx b/src/components/views/auth/PassphraseField.tsx
index 5175de6aa7..1050e8f6e2 100644
--- a/src/components/views/auth/PassphraseField.tsx
+++ b/src/components/views/auth/PassphraseField.tsx
@@ -23,7 +23,7 @@ import withValidation, { IFieldState, IValidationResult } from "../elements/Vali
 import { _t, _td } from "../../../languageHandler";
 import Field, { IInputProps } from "../elements/Field";
 
-interface IProps extends Omit<IInputProps, "onValidate"> {
+interface IProps extends Omit<IInputProps, "onValidate" | "element"> {
     autoFocus?: boolean;
     id?: string;
     className?: string;
diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx
index ad56409592..8392e6a14f 100644
--- a/src/components/views/context_menus/MessageContextMenu.tsx
+++ b/src/components/views/context_menus/MessageContextMenu.tsx
@@ -179,7 +179,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
 
     private isPinned(): boolean {
         const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
-        const pinnedEvent = room.currentState.getStateEvents(EventType.RoomPinnedEvents, "");
+        const pinnedEvent = room?.currentState.getStateEvents(EventType.RoomPinnedEvents, "");
         if (!pinnedEvent) return false;
         const content = pinnedEvent.getContent();
         return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
@@ -389,7 +389,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
             timelineRenderingType === TimelineRenderingType.ThreadsList;
         const isThreadRootEvent = isThread && mxEvent?.getThread()?.rootEvent === mxEvent;
 
-        let resendReactionsButton: JSX.Element;
+        let resendReactionsButton: JSX.Element | undefined;
         if (!mxEvent.isRedacted() && unsentReactionsCount !== 0) {
             resendReactionsButton = (
                 <IconizedContextMenuOption
@@ -400,7 +400,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
             );
         }
 
-        let redactButton: JSX.Element;
+        let redactButton: JSX.Element | undefined;
         if (isSent && this.state.canRedact) {
             redactButton = (
                 <IconizedContextMenuOption
@@ -411,7 +411,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
             );
         }
 
-        let openInMapSiteButton: JSX.Element;
+        let openInMapSiteButton: JSX.Element | undefined;
         const shareableLocationEvent = getShareableLocationEvent(mxEvent, cli);
         if (shareableLocationEvent) {
             const mapSiteLink = createMapSiteLinkFromEvent(shareableLocationEvent);
@@ -430,7 +430,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
             );
         }
 
-        let forwardButton: JSX.Element;
+        let forwardButton: JSX.Element | undefined;
         const forwardableEvent = getForwardableEvent(mxEvent, cli);
         if (contentActionable && forwardableEvent) {
             forwardButton = (
@@ -442,7 +442,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
             );
         }
 
-        let pinButton: JSX.Element;
+        let pinButton: JSX.Element | undefined;
         if (contentActionable && this.state.canPin) {
             pinButton = (
                 <IconizedContextMenuOption
@@ -462,7 +462,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
             />
         );
 
-        let unhidePreviewButton: JSX.Element;
+        let unhidePreviewButton: JSX.Element | undefined;
         if (eventTileOps?.isWidgetHidden()) {
             unhidePreviewButton = (
                 <IconizedContextMenuOption
@@ -473,7 +473,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
             );
         }
 
-        let permalinkButton: JSX.Element;
+        let permalinkButton: JSX.Element | undefined;
         if (permalink) {
             permalinkButton = (
                 <IconizedContextMenuOption
@@ -493,7 +493,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
             );
         }
 
-        let endPollButton: JSX.Element;
+        let endPollButton: JSX.Element | undefined;
         if (this.canEndPoll(mxEvent)) {
             endPollButton = (
                 <IconizedContextMenuOption
@@ -504,7 +504,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
             );
         }
 
-        let quoteButton: JSX.Element;
+        let quoteButton: JSX.Element | undefined;
         if (eventTileOps && canSendMessages) {
             // this event is rendered using TextualBody
             quoteButton = (
@@ -517,7 +517,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
         }
 
         // Bridges can provide a 'external_url' to link back to the source.
-        let externalURLButton: JSX.Element;
+        let externalURLButton: JSX.Element | undefined;
         if (
             typeof mxEvent.getContent().external_url === "string" &&
             isUrlPermitted(mxEvent.getContent().external_url)
@@ -540,7 +540,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
             );
         }
 
-        let collapseReplyChainButton: JSX.Element;
+        let collapseReplyChainButton: JSX.Element | undefined;
         if (collapseReplyChain) {
             collapseReplyChainButton = (
                 <IconizedContextMenuOption
@@ -551,7 +551,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
             );
         }
 
-        let jumpToRelatedEventButton: JSX.Element;
+        let jumpToRelatedEventButton: JSX.Element | undefined;
         const relatedEventId = mxEvent.getWireContent()?.["m.relates_to"]?.event_id;
         if (relatedEventId && SettingsStore.getValue("developerMode")) {
             jumpToRelatedEventButton = (
@@ -563,7 +563,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
             );
         }
 
-        let reportEventButton: JSX.Element;
+        let reportEventButton: JSX.Element | undefined;
         if (mxEvent.getSender() !== me) {
             reportEventButton = (
                 <IconizedContextMenuOption
@@ -574,7 +574,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
             );
         }
 
-        let copyLinkButton: JSX.Element;
+        let copyLinkButton: JSX.Element | undefined;
         if (link) {
             copyLinkButton = (
                 <IconizedContextMenuOption
@@ -594,7 +594,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
             );
         }
 
-        let copyButton: JSX.Element;
+        let copyButton: JSX.Element | undefined;
         if (rightClick && getSelectedText()) {
             copyButton = (
                 <IconizedContextMenuOption
@@ -606,7 +606,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
             );
         }
 
-        let editButton: JSX.Element;
+        let editButton: JSX.Element | undefined;
         if (rightClick && canEditContent(mxEvent)) {
             editButton = (
                 <IconizedContextMenuOption
@@ -617,7 +617,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
             );
         }
 
-        let replyButton: JSX.Element;
+        let replyButton: JSX.Element | undefined;
         if (rightClick && contentActionable && canSendMessages) {
             replyButton = (
                 <IconizedContextMenuOption
@@ -628,7 +628,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
             );
         }
 
-        let replyInThreadButton: JSX.Element;
+        let replyInThreadButton: JSX.Element | undefined;
         if (
             rightClick &&
             contentActionable &&
@@ -639,7 +639,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
             replyInThreadButton = <ReplyInThreadButton mxEvent={mxEvent} closeMenu={this.closeMenu} />;
         }
 
-        let reactButton;
+        let reactButton: JSX.Element | undefined;
         if (rightClick && contentActionable && canReact) {
             reactButton = (
                 <IconizedContextMenuOption
@@ -651,7 +651,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
             );
         }
 
-        let viewInRoomButton: JSX.Element;
+        let viewInRoomButton: JSX.Element | undefined;
         if (isThreadRootEvent) {
             viewInRoomButton = (
                 <IconizedContextMenuOption
@@ -662,7 +662,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
             );
         }
 
-        let nativeItemsList: JSX.Element;
+        let nativeItemsList: JSX.Element | undefined;
         if (copyButton || copyLinkButton) {
             nativeItemsList = (
                 <IconizedContextMenuOptionList>
@@ -672,7 +672,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
             );
         }
 
-        let quickItemsList: JSX.Element;
+        let quickItemsList: JSX.Element | undefined;
         if (editButton || replyButton || reactButton) {
             quickItemsList = (
                 <IconizedContextMenuOptionList>
@@ -703,12 +703,12 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
             </IconizedContextMenuOptionList>
         );
 
-        let redactItemList: JSX.Element;
+        let redactItemList: JSX.Element | undefined;
         if (redactButton) {
             redactItemList = <IconizedContextMenuOptionList red>{redactButton}</IconizedContextMenuOptionList>;
         }
 
-        let reactionPicker: JSX.Element;
+        let reactionPicker: JSX.Element | undefined;
         if (this.state.reactionPickerDisplayed) {
             const buttonRect = (this.reactButtonRef.current as HTMLElement)?.getBoundingClientRect();
             reactionPicker = (
diff --git a/src/components/views/dialogs/devtools/SettingExplorer.tsx b/src/components/views/dialogs/devtools/SettingExplorer.tsx
index c0801cd062..c3470ad030 100644
--- a/src/components/views/dialogs/devtools/SettingExplorer.tsx
+++ b/src/components/views/dialogs/devtools/SettingExplorer.tsx
@@ -64,7 +64,7 @@ interface ICanEditLevelFieldProps {
 }
 
 const CanEditLevelField: React.FC<ICanEditLevelFieldProps> = ({ setting, roomId, level }) => {
-    const canEdit = SettingsStore.canSetValue(setting, roomId, level);
+    const canEdit = SettingsStore.canSetValue(setting, roomId ?? null, level);
     const className = canEdit ? "mx_DevTools_SettingsExplorer_mutable" : "mx_DevTools_SettingsExplorer_immutable";
     return (
         <td className={className}>
diff --git a/src/components/views/directory/NetworkDropdown.tsx b/src/components/views/directory/NetworkDropdown.tsx
index b4f5b5bc04..c84bb4a2f1 100644
--- a/src/components/views/directory/NetworkDropdown.tsx
+++ b/src/components/views/directory/NetworkDropdown.tsx
@@ -44,7 +44,7 @@ const validServer = withValidation<undefined, { error?: MatrixError }>({
             // check if we can successfully load this server's room directory
             await MatrixClientPeg.get().publicRooms({
                 limit: 1,
-                server: value,
+                server: value ?? undefined,
             });
             return {};
         } catch (error) {
diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx
index 4bceab5ac9..10f18f1f31 100644
--- a/src/components/views/elements/AccessibleButton.tsx
+++ b/src/components/views/elements/AccessibleButton.tsx
@@ -14,7 +14,7 @@
  limitations under the License.
  */
 
-import React, { HTMLAttributes, InputHTMLAttributes, ReactHTML, ReactNode } from "react";
+import React, { HTMLAttributes, InputHTMLAttributes, ReactNode } from "react";
 import classnames from "classnames";
 
 import { getKeyBindingsManager } from "../../../KeyBindingsManager";
@@ -91,7 +91,7 @@ export interface IAccessibleButtonProps extends React.InputHTMLAttributes<Elemen
  * @returns {Object} rendered react
  */
 export default function AccessibleButton<T extends keyof JSX.IntrinsicElements>({
-    element,
+    element = "div" as T,
     onClick,
     children,
     kind,
@@ -169,7 +169,6 @@ export default function AccessibleButton<T extends keyof JSX.IntrinsicElements>(
 }
 
 AccessibleButton.defaultProps = {
-    element: "div" as keyof ReactHTML,
     role: "button",
     tabIndex: 0,
 };
diff --git a/src/components/views/elements/AppPermission.tsx b/src/components/views/elements/AppPermission.tsx
index 3b1c18a692..7adae9113b 100644
--- a/src/components/views/elements/AppPermission.tsx
+++ b/src/components/views/elements/AppPermission.tsx
@@ -61,8 +61,6 @@ export default class AppPermission extends React.Component<IProps, IState> {
 
         // Set all this into the initial state
         this.state = {
-            widgetDomain: null,
-            isWrapped: null,
             roomMember,
             ...urlInfo,
         };
diff --git a/src/components/views/elements/DesktopCapturerSourcePicker.tsx b/src/components/views/elements/DesktopCapturerSourcePicker.tsx
index cf8f6b018d..7be62a8999 100644
--- a/src/components/views/elements/DesktopCapturerSourcePicker.tsx
+++ b/src/components/views/elements/DesktopCapturerSourcePicker.tsx
@@ -33,7 +33,8 @@ export function getDesktopCapturerSources(): Promise<Array<DesktopCapturerSource
         },
         types: ["screen", "window"],
     };
-    return PlatformPeg.get().getDesktopCapturerSources(options);
+    const plaf = PlatformPeg.get();
+    return plaf ? plaf?.getDesktopCapturerSources(options) : Promise.resolve<DesktopCapturerSource[]>([]);
 }
 
 export enum Tabs {
diff --git a/src/components/views/elements/DialogButtons.tsx b/src/components/views/elements/DialogButtons.tsx
index 39c943b405..ddd5e274d0 100644
--- a/src/components/views/elements/DialogButtons.tsx
+++ b/src/components/views/elements/DialogButtons.tsx
@@ -69,7 +69,7 @@ export default class DialogButtons extends React.Component<IProps> {
     };
 
     private onCancelClick = (event: React.MouseEvent): void => {
-        this.props.onCancel(event);
+        this.props.onCancel?.(event);
     };
 
     public render(): React.ReactNode {
@@ -77,9 +77,9 @@ export default class DialogButtons extends React.Component<IProps> {
         if (this.props.primaryButtonClass) {
             primaryButtonClassName += " " + this.props.primaryButtonClass;
         }
-        let cancelButton;
 
-        if (this.props.cancelButton || this.props.hasCancel) {
+        let cancelButton: JSX.Element | undefined;
+        if (this.props.hasCancel) {
             cancelButton = (
                 <button
                     // important: the default type is 'submit' and this button comes before the
@@ -95,7 +95,7 @@ export default class DialogButtons extends React.Component<IProps> {
             );
         }
 
-        let additive = null;
+        let additive: JSX.Element | undefined;
         if (this.props.additive) {
             additive = <div className="mx_Dialog_buttons_additive">{this.props.additive}</div>;
         }
diff --git a/src/components/views/elements/Dropdown.tsx b/src/components/views/elements/Dropdown.tsx
index aecc8e8141..e16eaf978e 100644
--- a/src/components/views/elements/Dropdown.tsx
+++ b/src/components/views/elements/Dropdown.tsx
@@ -113,8 +113,8 @@ interface IState {
  */
 export default class Dropdown extends React.Component<DropdownProps, IState> {
     private readonly buttonRef = createRef<HTMLDivElement>();
-    private dropdownRootElement: HTMLDivElement = null;
-    private ignoreEvent: MouseEvent = null;
+    private dropdownRootElement: HTMLDivElement | null = null;
+    private ignoreEvent: MouseEvent | null = null;
     private childrenByKey: Record<string, ReactNode> = {};
 
     public constructor(props: DropdownProps) {
@@ -373,18 +373,14 @@ export default class Dropdown extends React.Component<DropdownProps, IState> {
             );
         }
 
-        const dropdownClasses: Record<string, boolean> = {
-            mx_Dropdown: true,
-            mx_Dropdown_disabled: this.props.disabled,
-        };
-        if (this.props.className) {
-            dropdownClasses[this.props.className] = true;
-        }
+        const dropdownClasses = classnames("mx_Dropdown", this.props.className, {
+            mx_Dropdown_disabled: !!this.props.disabled,
+        });
 
         // Note the menu sits inside the AccessibleButton div so it's anchored
         // to the input, but overflows below it. The root contains both.
         return (
-            <div className={classnames(dropdownClasses)} ref={this.collectRoot}>
+            <div className={dropdownClasses} ref={this.collectRoot}>
                 <AccessibleButton
                     className="mx_Dropdown_input mx_no_textinput"
                     onClick={this.onAccessibleButtonClick}
diff --git a/src/components/views/elements/EffectsOverlay.tsx b/src/components/views/elements/EffectsOverlay.tsx
index cf11350c2b..604b513d30 100644
--- a/src/components/views/elements/EffectsOverlay.tsx
+++ b/src/components/views/elements/EffectsOverlay.tsx
@@ -30,7 +30,7 @@ const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
     const canvasRef = useRef<HTMLCanvasElement>(null);
     const effectsRef = useRef<Map<string, ICanvasEffect>>(new Map<string, ICanvasEffect>());
 
-    const lazyLoadEffectModule = async (name: string): Promise<ICanvasEffect> => {
+    const lazyLoadEffectModule = async (name: string): Promise<ICanvasEffect | null> => {
         if (!name) return null;
         let effect: ICanvasEffect | null = effectsRef.current.get(name) || null;
         if (effect === null) {
@@ -38,7 +38,7 @@ const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
             try {
                 const { default: Effect } = await import(`../../../effects/${name}`);
                 effect = new Effect(options);
-                effectsRef.current.set(name, effect);
+                effectsRef.current.set(name, effect!);
             } catch (err) {
                 logger.warn(`Unable to load effect module at '../../../effects/${name}.`, err);
             }
@@ -70,7 +70,7 @@ const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
             // eslint-disable-next-line react-hooks/exhaustive-deps
             const currentEffects = effectsRef.current; // this is not a react node ref, warning can be safely ignored
             for (const effect in currentEffects) {
-                const effectModule: ICanvasEffect = currentEffects.get(effect);
+                const effectModule: ICanvasEffect = currentEffects.get(effect)!;
                 if (effectModule && effectModule.isRunning) {
                     effectModule.stop();
                 }
diff --git a/src/components/views/elements/ErrorBoundary.tsx b/src/components/views/elements/ErrorBoundary.tsx
index 3653415db6..a230ab84c5 100644
--- a/src/components/views/elements/ErrorBoundary.tsx
+++ b/src/components/views/elements/ErrorBoundary.tsx
@@ -30,7 +30,7 @@ interface Props {
 }
 
 interface IState {
-    error: Error;
+    error?: Error;
 }
 
 /**
@@ -41,9 +41,7 @@ export default class ErrorBoundary extends React.PureComponent<Props, IState> {
     public constructor(props: Props) {
         super(props);
 
-        this.state = {
-            error: null,
-        };
+        this.state = {};
     }
 
     public static getDerivedStateFromError(error: Error): Partial<IState> {
@@ -66,7 +64,7 @@ export default class ErrorBoundary extends React.PureComponent<Props, IState> {
         MatrixClientPeg.get()
             .store.deleteAllData()
             .then(() => {
-                PlatformPeg.get().reload();
+                PlatformPeg.get()?.reload();
             });
     };
 
@@ -121,7 +119,7 @@ export default class ErrorBoundary extends React.PureComponent<Props, IState> {
                 );
             }
 
-            let clearCacheButton: JSX.Element;
+            let clearCacheButton: JSX.Element | undefined;
             // we only show this button if there is an initialised MatrixClient otherwise we can't clear the cache
             if (MatrixClientPeg.get()) {
                 clearCacheButton = (
diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx
index 1ba14bb051..5498773777 100644
--- a/src/components/views/elements/EventListSummary.tsx
+++ b/src/components/views/elements/EventListSummary.tsx
@@ -113,7 +113,7 @@ export default class EventListSummary extends React.Component<IProps> {
     private generateSummary(
         eventAggregates: Record<string, string[]>,
         orderedTransitionSequences: string[],
-    ): string | JSX.Element {
+    ): ReactNode {
         const summaries = orderedTransitionSequences.map((transitions) => {
             const userNames = eventAggregates[transitions];
             const nameList = this.renderNameList(userNames);
@@ -392,7 +392,7 @@ export default class EventListSummary extends React.Component<IProps> {
      * @returns {string?} the transition type given to this event. This defaults to `null`
      * if a transition is not recognised.
      */
-    private static getTransition(e: IUserEvents): TransitionType {
+    private static getTransition(e: IUserEvents): TransitionType | null {
         if (e.mxEvent.isRedacted()) {
             return TransitionType.MessageRemoved;
         }
diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx
index 2e533e1925..c5e531360f 100644
--- a/src/components/views/elements/Field.tsx
+++ b/src/components/views/elements/Field.tsx
@@ -79,7 +79,7 @@ export interface IInputProps extends IProps, InputHTMLAttributes<HTMLInputElemen
     // The ref pass through to the input
     inputRef?: RefObject<HTMLInputElement>;
     // The element to create. Defaults to "input".
-    element?: "input";
+    element: "input";
     // The input's value. This is a controlled component, so the value is required.
     value: string;
 }
@@ -204,7 +204,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
         const value = this.inputRef.current?.value ?? null;
         const { valid, feedback } = await this.props.onValidate({
             value,
-            focused,
+            focused: !!focused,
             allowEmpty,
         });
 
diff --git a/src/components/views/elements/GenericEventListSummary.tsx b/src/components/views/elements/GenericEventListSummary.tsx
index c90c64d740..83d12a360b 100644
--- a/src/components/views/elements/GenericEventListSummary.tsx
+++ b/src/components/views/elements/GenericEventListSummary.tsx
@@ -36,7 +36,7 @@ interface IProps {
     // The list of room members for which to show avatars next to the summary
     "summaryMembers"?: RoomMember[];
     // The text to show as the summary of this event list
-    "summaryText"?: string | JSX.Element;
+    "summaryText"?: ReactNode;
     // An array of EventTiles to render when expanded
     "children": ReactNode[];
     // Called when the event list expansion is toggled
diff --git a/src/components/views/elements/IRCTimelineProfileResizer.tsx b/src/components/views/elements/IRCTimelineProfileResizer.tsx
index af1585e997..48bf1a188d 100644
--- a/src/components/views/elements/IRCTimelineProfileResizer.tsx
+++ b/src/components/views/elements/IRCTimelineProfileResizer.tsx
@@ -29,7 +29,7 @@ interface IProps {
 
 interface IState {
     width: number;
-    IRCLayoutRoot: HTMLElement;
+    IRCLayoutRoot: HTMLElement | null;
 }
 
 export default class IRCTimelineProfileResizer extends React.Component<IProps, IState> {
@@ -77,7 +77,7 @@ export default class IRCTimelineProfileResizer extends React.Component<IProps, I
     };
 
     private updateCSSWidth(newWidth: number): void {
-        this.state.IRCLayoutRoot.style.setProperty("--name-width", newWidth + "px");
+        this.state.IRCLayoutRoot?.style.setProperty("--name-width", newWidth + "px");
     }
 
     private onMoueUp = (): void => {
diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx
index 325c54304a..d8cf6c5ef7 100644
--- a/src/components/views/elements/ImageView.tsx
+++ b/src/components/views/elements/ImageView.tsx
@@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React, { createRef } from "react";
+import React, { createRef, CSSProperties } from "react";
 import FocusLock from "react-focus-lock";
 import { MatrixEvent } from "matrix-js-sdk/src/models/event";
 
@@ -395,7 +395,7 @@ export default class ImageView extends React.Component<IProps, IState> {
     };
 
     private renderContextMenu(): JSX.Element {
-        let contextMenu = null;
+        let contextMenu: JSX.Element | undefined;
         if (this.state.contextMenuDisplayed) {
             contextMenu = (
                 <MessageContextMenu
@@ -419,11 +419,6 @@ export default class ImageView extends React.Component<IProps, IState> {
         else if (this.state.moving || !this.imageIsLoaded) transitionClassName = "";
         else transitionClassName = "mx_ImageView_image_animating";
 
-        let cursor;
-        if (this.state.moving) cursor = "grabbing";
-        else if (this.state.zoom === this.state.minZoom) cursor = "zoom-in";
-        else cursor = "zoom-out";
-
         const rotationDegrees = this.state.rotation + "deg";
         const zoom = this.state.zoom;
         const translatePixelsX = this.state.translationX + "px";
@@ -432,15 +427,18 @@ export default class ImageView extends React.Component<IProps, IState> {
         // First, we translate and only then we rotate, otherwise
         // we would apply the translation to an already rotated
         // image causing it translate in the wrong direction.
-        const style = {
-            cursor: cursor,
+        const style: CSSProperties = {
             transform: `translateX(${translatePixelsX})
                         translateY(${translatePixelsY})
                         scale(${zoom})
                         rotate(${rotationDegrees})`,
         };
 
-        let info;
+        if (this.state.moving) style.cursor = "grabbing";
+        else if (this.state.zoom === this.state.minZoom) style.cursor = "zoom-in";
+        else style.cursor = "zoom-out";
+
+        let info: JSX.Element | undefined;
         if (showEventMeta) {
             const mxEvent = this.props.mxEvent;
             const showTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps");
@@ -449,7 +447,7 @@ export default class ImageView extends React.Component<IProps, IState> {
                 permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
             }
 
-            const senderName = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
+            const senderName = mxEvent.sender?.name ?? mxEvent.getSender();
             const sender = <div className="mx_ImageView_info_sender">{senderName}</div>;
             const messageTimestamp = (
                 <a
@@ -491,7 +489,7 @@ export default class ImageView extends React.Component<IProps, IState> {
             info = <div />;
         }
 
-        let contextMenuButton;
+        let contextMenuButton: JSX.Element | undefined;
         if (this.props.mxEvent) {
             contextMenuButton = (
                 <ContextMenuTooltipButton
@@ -519,7 +517,7 @@ export default class ImageView extends React.Component<IProps, IState> {
             />
         );
 
-        let title: JSX.Element;
+        let title: JSX.Element | undefined;
         if (this.props.mxEvent?.getContent()) {
             title = (
                 <div className="mx_ImageView_title">
diff --git a/src/components/views/elements/InteractiveTooltip.tsx b/src/components/views/elements/InteractiveTooltip.tsx
index 1986de0243..3aecd8a743 100644
--- a/src/components/views/elements/InteractiveTooltip.tsx
+++ b/src/components/views/elements/InteractiveTooltip.tsx
@@ -293,7 +293,7 @@ interface IProps {
 }
 
 interface IState {
-    contentRect: DOMRect;
+    contentRect?: DOMRect;
     visible: boolean;
 }
 
@@ -312,7 +312,6 @@ export default class InteractiveTooltip extends React.Component<IProps, IState>
         super(props);
 
         this.state = {
-            contentRect: null,
             visible: false,
         };
     }
@@ -331,7 +330,7 @@ export default class InteractiveTooltip extends React.Component<IProps, IState>
         document.removeEventListener("mousemove", this.onMouseMove);
     }
 
-    private collectContentRect = (element: HTMLElement): void => {
+    private collectContentRect = (element: HTMLElement | null): void => {
         // We don't need to clean up when unmounting, so ignore
         if (!element) return;
 
@@ -354,7 +353,7 @@ export default class InteractiveTooltip extends React.Component<IProps, IState>
         } else {
             const targetRight = targetRect.right + window.scrollX;
             const spaceOnRight = UIStore.instance.windowWidth - targetRight;
-            return contentRect && spaceOnRight - contentRect.width < MIN_SAFE_DISTANCE_TO_WINDOW_EDGE;
+            return !!contentRect && spaceOnRight - contentRect.width < MIN_SAFE_DISTANCE_TO_WINDOW_EDGE;
         }
     }
 
@@ -368,7 +367,7 @@ export default class InteractiveTooltip extends React.Component<IProps, IState>
         } else {
             const targetBottom = targetRect.bottom + window.scrollY;
             const spaceBelow = UIStore.instance.windowHeight - targetBottom;
-            return contentRect && spaceBelow - contentRect.height < MIN_SAFE_DISTANCE_TO_WINDOW_EDGE;
+            return !!contentRect && spaceBelow - contentRect.height < MIN_SAFE_DISTANCE_TO_WINDOW_EDGE;
         }
     }
 
@@ -416,7 +415,7 @@ export default class InteractiveTooltip extends React.Component<IProps, IState>
         document.removeEventListener("mousemove", this.onMouseMove);
     }
 
-    private renderTooltip(): JSX.Element {
+    private renderTooltip(): ReactNode {
         const { contentRect, visible } = this.state;
         if (!visible) {
             ReactDOM.unmountComponentAtNode(getOrCreateContainer());
@@ -435,7 +434,7 @@ export default class InteractiveTooltip extends React.Component<IProps, IState>
         // tooltip content would extend past the safe area towards the window
         // edge, flip around to below the target.
         const position: Partial<IRect> = {};
-        let chevronFace: ChevronFace = null;
+        let chevronFace: ChevronFace | null = null;
         if (this.isOnTheSide) {
             if (this.onLeftOfTarget()) {
                 position.left = targetLeft;
@@ -461,8 +460,7 @@ export default class InteractiveTooltip extends React.Component<IProps, IState>
 
         const chevron = <div className={"mx_InteractiveTooltip_chevron_" + chevronFace} />;
 
-        const menuClasses = classNames({
-            mx_InteractiveTooltip: true,
+        const menuClasses = classNames("mx_InteractiveTooltip", {
             mx_InteractiveTooltip_withChevron_top: chevronFace === ChevronFace.Top,
             mx_InteractiveTooltip_withChevron_left: chevronFace === ChevronFace.Left,
             mx_InteractiveTooltip_withChevron_right: chevronFace === ChevronFace.Right,
diff --git a/src/components/views/elements/LanguageDropdown.tsx b/src/components/views/elements/LanguageDropdown.tsx
index ee03774cbc..178d85c4eb 100644
--- a/src/components/views/elements/LanguageDropdown.tsx
+++ b/src/components/views/elements/LanguageDropdown.tsx
@@ -40,7 +40,7 @@ interface IProps {
 
 interface IState {
     searchQuery: string;
-    langs: Languages;
+    langs: Languages | null;
 }
 
 export default class LanguageDropdown extends React.Component<IProps, IState> {
@@ -103,8 +103,8 @@ export default class LanguageDropdown extends React.Component<IProps, IState> {
 
         // default value here too, otherwise we need to handle null / undefined
         // values between mounting and the initial value propagating
-        let language = SettingsStore.getValue("language", null, /*excludeDefault:*/ true);
-        let value = null;
+        let language = SettingsStore.getValue<string | undefined>("language", null, /*excludeDefault:*/ true);
+        let value: string | undefined;
         if (language) {
             value = this.props.value || language;
         } else {
diff --git a/src/components/views/elements/LazyRenderList.tsx b/src/components/views/elements/LazyRenderList.tsx
index e14941bd73..7fcbe1be2c 100644
--- a/src/components/views/elements/LazyRenderList.tsx
+++ b/src/components/views/elements/LazyRenderList.tsx
@@ -76,7 +76,7 @@ interface IProps<T> {
 }
 
 interface IState {
-    renderRange: ItemRange;
+    renderRange: ItemRange | null;
 }
 
 export default class LazyRenderList<T = any> extends React.Component<IProps<T>, IState> {
@@ -93,7 +93,7 @@ export default class LazyRenderList<T = any> extends React.Component<IProps<T>,
         };
     }
 
-    public static getDerivedStateFromProps(props: IProps<unknown>, state: IState): Partial<IState> {
+    public static getDerivedStateFromProps(props: IProps<unknown>, state: IState): Partial<IState> | null {
         const range = LazyRenderList.getVisibleRangeFromProps(props);
         const intersectRange = range.expand(props.overflowMargin);
         const renderRange = range.expand(props.overflowItems);
diff --git a/src/components/views/elements/MiniAvatarUploader.tsx b/src/components/views/elements/MiniAvatarUploader.tsx
index ec98fea43c..18652a4c62 100644
--- a/src/components/views/elements/MiniAvatarUploader.tsx
+++ b/src/components/views/elements/MiniAvatarUploader.tsx
@@ -64,7 +64,8 @@ const MiniAvatarUploader: React.FC<IProps> = ({
     const label = hasAvatar || busy ? hasAvatarLabel : noAvatarLabel;
 
     const { room } = useContext(RoomContext);
-    const canSetAvatar = isUserAvatar || room?.currentState?.maySendStateEvent(EventType.RoomAvatar, cli.getUserId());
+    const canSetAvatar =
+        isUserAvatar || room?.currentState?.maySendStateEvent(EventType.RoomAvatar, cli.getSafeUserId());
     if (!canSetAvatar) return <React.Fragment>{children}</React.Fragment>;
 
     const visible = !!label && (hover || show);
diff --git a/src/components/views/elements/PersistentApp.tsx b/src/components/views/elements/PersistentApp.tsx
index 67ad09018d..a13588b96b 100644
--- a/src/components/views/elements/PersistentApp.tsx
+++ b/src/components/views/elements/PersistentApp.tsx
@@ -50,7 +50,7 @@ export default class PersistentApp extends React.Component<IProps> {
                 app={app}
                 fullWidth={true}
                 room={this.room}
-                userId={this.context.credentials.userId}
+                userId={this.context.getSafeUserId()}
                 creatorUserId={app.creatorUserId}
                 widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
                 waitForIframeLoad={app.waitForIframeLoad}
diff --git a/src/components/views/elements/PollCreateDialog.tsx b/src/components/views/elements/PollCreateDialog.tsx
index a30453ed87..dbade2d078 100644
--- a/src/components/views/elements/PollCreateDialog.tsx
+++ b/src/components/views/elements/PollCreateDialog.tsx
@@ -163,7 +163,12 @@ export default class PollCreateDialog extends ScrollableBaseModal<IProps, IState
         doMaybeLocalRoomAction(
             this.props.room.roomId,
             (actualRoomId: string) =>
-                this.matrixClient.sendEvent(actualRoomId, this.props.threadId, pollEvent.type, pollEvent.content),
+                this.matrixClient.sendEvent(
+                    actualRoomId,
+                    this.props.threadId ?? null,
+                    pollEvent.type,
+                    pollEvent.content,
+                ),
             this.matrixClient,
         )
             .then(() => this.props.onFinished(true))
diff --git a/src/components/views/elements/ReplyChain.tsx b/src/components/views/elements/ReplyChain.tsx
index 9f80f4cf76..be415027c4 100644
--- a/src/components/views/elements/ReplyChain.tsx
+++ b/src/components/views/elements/ReplyChain.tsx
@@ -48,7 +48,7 @@ interface IProps {
     parentEv?: MatrixEvent;
     // called when the ReplyChain contents has changed, including EventTiles thereof
     onHeightChanged: () => void;
-    permalinkCreator: RoomPermalinkCreator;
+    permalinkCreator?: RoomPermalinkCreator;
     // Specifies which layout to use.
     layout?: Layout;
     // Whether to always show a timestamp
diff --git a/src/components/views/elements/RoomAliasField.tsx b/src/components/views/elements/RoomAliasField.tsx
index b2e6965c63..10549d79db 100644
--- a/src/components/views/elements/RoomAliasField.tsx
+++ b/src/components/views/elements/RoomAliasField.tsx
@@ -71,7 +71,7 @@ export default class RoomAliasField extends React.PureComponent<IProps, IState>
         const postfix = domain ? <span title={`:${domain}`}>{`:${domain}`}</span> : <span />;
         const maxlength = domain ? 255 - domain.length - 2 : 255 - 1; // 2 for # and :
         const value = domain
-            ? this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1)
+            ? this.props.value.substring(1, this.props.value.length - domain.length - 1)
             : this.props.value.substring(1);
 
         return { prefix, postfix, value, maxlength };
@@ -104,7 +104,7 @@ export default class RoomAliasField extends React.PureComponent<IProps, IState>
 
     private onValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
         const result = await this.validationRules(fieldState);
-        this.setState({ isValid: result.valid });
+        this.setState({ isValid: !!result.valid });
         return result;
     };
 
@@ -225,8 +225,9 @@ export default class RoomAliasField extends React.PureComponent<IProps, IState>
         return this.state.isValid;
     }
 
-    public validate(options: IValidateOpts): Promise<boolean> {
-        return this.fieldRef.current?.validate(options);
+    public async validate(options: IValidateOpts): Promise<boolean> {
+        const val = await this.fieldRef.current?.validate(options);
+        return val ?? false;
     }
 
     public focus(): void {
diff --git a/src/components/views/elements/RoomTopic.tsx b/src/components/views/elements/RoomTopic.tsx
index e760d38218..7aa6bce949 100644
--- a/src/components/views/elements/RoomTopic.tsx
+++ b/src/components/views/elements/RoomTopic.tsx
@@ -33,7 +33,7 @@ import TooltipTarget from "./TooltipTarget";
 import { Linkify, topicToHtml } from "../../../HtmlUtils";
 
 interface IProps extends React.HTMLProps<HTMLDivElement> {
-    room?: Room;
+    room: Room;
 }
 
 export default function RoomTopic({ room, ...props }: IProps): JSX.Element {
@@ -62,7 +62,7 @@ export default function RoomTopic({ room, ...props }: IProps): JSX.Element {
 
     useDispatcher(dis, (payload) => {
         if (payload.action === Action.ShowRoomTopic) {
-            const canSetTopic = room.currentState.maySendStateEvent(EventType.RoomTopic, client.getUserId());
+            const canSetTopic = room.currentState.maySendStateEvent(EventType.RoomTopic, client.getSafeUserId());
             const body = topicToHtml(topic?.text, topic?.html, ref, true);
 
             const modal = Modal.createDialog(InfoDialog, {
diff --git a/src/components/views/elements/SearchWarning.tsx b/src/components/views/elements/SearchWarning.tsx
index 14ffcbd510..421f550ff6 100644
--- a/src/components/views/elements/SearchWarning.tsx
+++ b/src/components/views/elements/SearchWarning.tsx
@@ -71,7 +71,7 @@ export default function SearchWarning({ isRoomEncrypted, kind }: IProps): JSX.El
 
     let text: ReactNode | undefined;
     let logo: JSX.Element | undefined;
-    if (desktopBuilds.get("available")) {
+    if (desktopBuilds?.get("available")) {
         logo = <img src={desktopBuilds.get("logo")} />;
         const buildUrl = desktopBuilds.get("url");
         switch (kind) {
diff --git a/src/components/views/elements/ServerPicker.tsx b/src/components/views/elements/ServerPicker.tsx
index adfb263fa5..f940150a9e 100644
--- a/src/components/views/elements/ServerPicker.tsx
+++ b/src/components/views/elements/ServerPicker.tsx
@@ -33,7 +33,7 @@ interface IProps {
 }
 
 const showPickerDialog = (
-    title: string,
+    title: string | undefined,
     serverConfig: ValidatedServerConfig,
     onFinished: (config: ValidatedServerConfig) => void,
 ): void => {
diff --git a/src/components/views/elements/SettingsFlag.tsx b/src/components/views/elements/SettingsFlag.tsx
index 87a6b38df4..b1e6cf41c0 100644
--- a/src/components/views/elements/SettingsFlag.tsx
+++ b/src/components/views/elements/SettingsFlag.tsx
@@ -69,14 +69,14 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
     private save = async (val?: boolean): Promise<void> => {
         await SettingsStore.setValue(
             this.props.name,
-            this.props.roomId,
+            this.props.roomId ?? null,
             this.props.level,
             val !== undefined ? val : this.state.value,
         );
     };
 
     public render(): React.ReactNode {
-        const canChange = SettingsStore.canSetValue(this.props.name, this.props.roomId, this.props.level);
+        const canChange = SettingsStore.canSetValue(this.props.name, this.props.roomId ?? null, this.props.level);
 
         if (!canChange && this.props.hideIfCannotSet) return null;
 
diff --git a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
index f4fd7fe9dc..7a725cf2d4 100644
--- a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
+++ b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
@@ -38,7 +38,7 @@ interface SpellCheckLanguagesDropdownIProps {
 
 interface SpellCheckLanguagesDropdownIState {
     searchQuery: string;
-    languages: Languages;
+    languages?: Languages;
 }
 
 export default class SpellCheckLanguagesDropdown extends React.Component<
@@ -51,7 +51,6 @@ export default class SpellCheckLanguagesDropdown extends React.Component<
 
         this.state = {
             searchQuery: "",
-            languages: null,
         };
     }
 
@@ -59,7 +58,7 @@ export default class SpellCheckLanguagesDropdown extends React.Component<
         const plaf = PlatformPeg.get();
         if (plaf) {
             plaf.getAvailableSpellCheckLanguages()
-                .then((languages) => {
+                ?.then((languages) => {
                     languages.sort(function (a, b) {
                         if (a < b) return -1;
                         if (a > b) return 1;
@@ -92,11 +91,11 @@ export default class SpellCheckLanguagesDropdown extends React.Component<
     }
 
     public render(): React.ReactNode {
-        if (this.state.languages === null) {
+        if (!this.state.languages) {
             return <Spinner />;
         }
 
-        let displayedLanguages;
+        let displayedLanguages: Languages;
         if (this.state.searchQuery) {
             displayedLanguages = this.state.languages.filter((lang) => {
                 return languageMatchesSearchQuery(this.state.searchQuery, lang);
@@ -111,8 +110,8 @@ export default class SpellCheckLanguagesDropdown extends React.Component<
 
         // default value here too, otherwise we need to handle null / undefined;
         // values between mounting and the initial value propagating
-        let language = SettingsStore.getValue("language", null, /*excludeDefault:*/ true);
-        let value = null;
+        let language = SettingsStore.getValue<string | undefined>("language", null, /*excludeDefault:*/ true);
+        let value: string | undefined;
         if (language) {
             value = this.props.value || language;
         } else {
diff --git a/src/components/views/elements/TextWithTooltip.tsx b/src/components/views/elements/TextWithTooltip.tsx
index e54ed077c5..3ebdac5511 100644
--- a/src/components/views/elements/TextWithTooltip.tsx
+++ b/src/components/views/elements/TextWithTooltip.tsx
@@ -24,7 +24,7 @@ interface IProps extends HTMLAttributes<HTMLSpanElement> {
     tooltipClass?: string;
     tooltip: React.ReactNode;
     tooltipProps?: Omit<React.ComponentProps<typeof TooltipTarget>, "label" | "tooltipClassName" | "className">;
-    onClick?: (ev?: React.MouseEvent) => void;
+    onClick?: (ev: React.MouseEvent) => void;
 }
 
 export default class TextWithTooltip extends React.Component<IProps> {
diff --git a/src/components/views/elements/UseCaseSelection.tsx b/src/components/views/elements/UseCaseSelection.tsx
index 1e6cf09e75..2f7f171fad 100644
--- a/src/components/views/elements/UseCaseSelection.tsx
+++ b/src/components/views/elements/UseCaseSelection.tsx
@@ -40,7 +40,7 @@ export function UseCaseSelection({ onFinished }: Props): JSX.Element {
                 onFinished(selection);
             }, TIMEOUT);
             return () => {
-                clearTimeout(handler);
+                if (handler !== null) clearTimeout(handler);
                 handler = null;
             };
         }
diff --git a/src/components/views/elements/UseCaseSelectionButton.tsx b/src/components/views/elements/UseCaseSelectionButton.tsx
index 13203b5864..5cfe26c734 100644
--- a/src/components/views/elements/UseCaseSelectionButton.tsx
+++ b/src/components/views/elements/UseCaseSelectionButton.tsx
@@ -28,7 +28,7 @@ interface Props {
 }
 
 export function UseCaseSelectionButton({ useCase, onClick, selected }: Props): JSX.Element {
-    let label: string;
+    let label: string | undefined;
     switch (useCase) {
         case UseCase.PersonalMessaging:
             label = _t("Friends and family");
diff --git a/src/components/views/elements/Validation.tsx b/src/components/views/elements/Validation.tsx
index db51c46bae..b1101f41dc 100644
--- a/src/components/views/elements/Validation.tsx
+++ b/src/components/views/elements/Validation.tsx
@@ -43,7 +43,7 @@ interface IArgs<T, D = void> {
 }
 
 export interface IFieldState {
-    value: string;
+    value: string | null;
     focused: boolean;
     allowEmpty?: boolean;
 }
diff --git a/src/components/views/messages/DownloadActionButton.tsx b/src/components/views/messages/DownloadActionButton.tsx
index b6b8adfaa4..53cdf74b32 100644
--- a/src/components/views/messages/DownloadActionButton.tsx
+++ b/src/components/views/messages/DownloadActionButton.tsx
@@ -80,7 +80,7 @@ export default class DownloadActionButton extends React.PureComponent<IProps, IS
     }
 
     public render(): React.ReactNode {
-        let spinner: JSX.Element;
+        let spinner: JSX.Element | undefined;
         if (this.state.loading) {
             spinner = <Spinner w={18} h={18} />;
         }
diff --git a/src/components/views/messages/EditHistoryMessage.tsx b/src/components/views/messages/EditHistoryMessage.tsx
index ec10869a71..906867d6a8 100644
--- a/src/components/views/messages/EditHistoryMessage.tsx
+++ b/src/components/views/messages/EditHistoryMessage.tsx
@@ -47,7 +47,7 @@ interface IProps {
 
 interface IState {
     canRedact: boolean;
-    sendStatus: EventStatus;
+    sendStatus: EventStatus | null;
 }
 
 export default class EditHistoryMessage extends React.PureComponent<IProps, IState> {
@@ -59,12 +59,10 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
         super(props);
 
         const cli = MatrixClientPeg.get();
-        const { userId } = cli.credentials;
+        const userId = cli.getSafeUserId();
         const event = this.props.mxEvent;
         const room = cli.getRoom(event.getRoomId());
-        if (event.localRedactionEvent()) {
-            event.localRedactionEvent().on(MatrixEventEvent.Status, this.onAssociatedStatusChanged);
-        }
+        event.localRedactionEvent()?.on(MatrixEventEvent.Status, this.onAssociatedStatusChanged);
         const canRedact = room.currentState.maySendRedactionForEvent(event, userId);
         this.state = { canRedact, sendStatus: event.getAssociatedStatus() };
     }
@@ -121,9 +119,7 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
         unmountPills(this.pills);
         unmountTooltips(this.tooltips);
         const event = this.props.mxEvent;
-        if (event.localRedactionEvent()) {
-            event.localRedactionEvent().off(MatrixEventEvent.Status, this.onAssociatedStatusChanged);
-        }
+        event.localRedactionEvent()?.off(MatrixEventEvent.Status, this.onAssociatedStatusChanged);
     }
 
     public componentDidUpdate(): void {
@@ -133,12 +129,12 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
 
     private renderActionBar(): JSX.Element {
         // hide the button when already redacted
-        let redactButton: JSX.Element;
+        let redactButton: JSX.Element | undefined;
         if (!this.props.mxEvent.isRedacted() && !this.props.isBaseEvent && this.state.canRedact) {
             redactButton = <AccessibleButton onClick={this.onRedactClick}>{_t("Remove")}</AccessibleButton>;
         }
 
-        let viewSourceButton: JSX.Element;
+        let viewSourceButton: JSX.Element | undefined;
         if (SettingsStore.getValue("developerMode")) {
             viewSourceButton = (
                 <AccessibleButton onClick={this.onViewSourceClick}>{_t("View Source")}</AccessibleButton>
@@ -189,9 +185,8 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
         }
 
         const timestamp = formatTime(new Date(mxEvent.getTs()), this.props.isTwelveHour);
-        const isSending = ["sending", "queued", "encrypting"].indexOf(this.state.sendStatus) !== -1;
-        const classes = classNames({
-            mx_EventTile: true,
+        const isSending = ["sending", "queued", "encrypting"].includes(this.state.sendStatus!);
+        const classes = classNames("mx_EventTile", {
             // Note: we keep the `sending` state class for tests, not for our styles
             mx_EventTile_sending: isSending,
         });
diff --git a/src/components/views/messages/JumpToDatePicker.tsx b/src/components/views/messages/JumpToDatePicker.tsx
index ef6c39bd6f..fa13235809 100644
--- a/src/components/views/messages/JumpToDatePicker.tsx
+++ b/src/components/views/messages/JumpToDatePicker.tsx
@@ -22,7 +22,7 @@ import { RovingAccessibleButton, useRovingTabIndex } from "../../../accessibilit
 
 interface IProps {
     ts: number;
-    onDatePicked?: (dateString: string) => void;
+    onDatePicked: (dateString: string) => void;
 }
 
 const JumpToDatePicker: React.FC<IProps> = ({ ts, onDatePicked }: IProps) => {
diff --git a/src/components/views/messages/LegacyCallEvent.tsx b/src/components/views/messages/LegacyCallEvent.tsx
index 4678b1a2e0..a8ba902e61 100644
--- a/src/components/views/messages/LegacyCallEvent.tsx
+++ b/src/components/views/messages/LegacyCallEvent.tsx
@@ -165,7 +165,7 @@ export default class LegacyCallEvent extends React.PureComponent<IProps, IState>
                         {this.props.timestamp}
                     </div>
                 );
-            } else if ([CallErrorCode.UserHangup, "user hangup"].includes(hangupReason) || !hangupReason) {
+            } else if (!hangupReason || [CallErrorCode.UserHangup, "user hangup"].includes(hangupReason)) {
                 // workaround for https://github.com/vector-im/element-web/issues/5178
                 // it seems Android randomly sets a reason of "user hangup" which is
                 // interpreted as an error code :(
diff --git a/src/components/views/messages/MBeaconBody.tsx b/src/components/views/messages/MBeaconBody.tsx
index 363b69f7d4..3fa0b60696 100644
--- a/src/components/views/messages/MBeaconBody.tsx
+++ b/src/components/views/messages/MBeaconBody.tsx
@@ -140,7 +140,7 @@ const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent, getRelati
         error?.message === LocationShareError.MapStyleUrlNotConfigured ||
         error?.message === LocationShareError.MapStyleUrlNotReachable;
     const displayStatus = getBeaconDisplayStatus(
-        isLive,
+        !!isLive,
         latestLocationState,
         // if we are unable to display maps because it is not configured for the server
         // don't display an error
@@ -174,7 +174,7 @@ const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent, getRelati
         map = (
             <Map
                 id={mapId}
-                centerGeoUri={latestLocationState.uri}
+                centerGeoUri={latestLocationState?.uri}
                 onError={setError}
                 onClick={onClick}
                 className="mx_MBeaconBody_map"
@@ -184,7 +184,7 @@ const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent, getRelati
                         map={map}
                         id={`${mapId}-marker`}
                         geoUri={latestLocationState.uri}
-                        roomMember={markerRoomMember}
+                        roomMember={markerRoomMember ?? undefined}
                         useMemberColor
                     />
                 )}
diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx
index 67747fe1e2..f556b59cf7 100644
--- a/src/components/views/messages/MImageBody.tsx
+++ b/src/components/views/messages/MImageBody.tsx
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React, { ComponentProps, createRef } from "react";
+import React, { ComponentProps, createRef, ReactNode } from "react";
 import { Blurhash } from "react-blurhash";
 import classNames from "classnames";
 import { CSSTransition, SwitchTransition } from "react-transition-group";
@@ -47,8 +47,8 @@ enum Placeholder {
 }
 
 interface IState {
-    contentUrl?: string;
-    thumbUrl?: string;
+    contentUrl: string | null;
+    thumbUrl: string | null;
     isAnimated?: boolean;
     error?: Error;
     imgError: boolean;
@@ -78,6 +78,8 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
         this.reconnectedListener = createReconnectedListener(this.clearError);
 
         this.state = {
+            contentUrl: null,
+            thumbUrl: null,
             imgError: false,
             imgLoaded: false,
             hover: false,
@@ -126,7 +128,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
                 };
             }
 
-            Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
+            Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", undefined, true);
         }
     };
 
@@ -177,7 +179,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
         this.setState({ imgLoaded: true, loadedImageDimensions });
     };
 
-    private getContentUrl(): string {
+    private getContentUrl(): string | null {
         // During export, the content url will point to the MSC, which will later point to a local url
         if (this.props.forExport) return this.media.srcMxc;
         return this.media.srcHttp;
@@ -187,7 +189,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
         return mediaFromContent(this.props.mxEvent.getContent());
     }
 
-    private getThumbUrl(): string {
+    private getThumbUrl(): string | null {
         // FIXME: we let images grow as wide as you like, rather than capped to 800x600.
         // So either we need to support custom timeline widths here, or reimpose the cap, otherwise the
         // thumbnail resolution will be unnecessarily reduced.
@@ -242,8 +244,8 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
     private async downloadImage(): Promise<void> {
         if (this.state.contentUrl) return; // already downloaded
 
-        let thumbUrl: string;
-        let contentUrl: string;
+        let thumbUrl: string | null;
+        let contentUrl: string | null;
         if (this.props.mediaEventHelper.media.isEncrypted) {
             try {
                 [contentUrl, thumbUrl] = await Promise.all([
@@ -276,7 +278,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
         // If there is no included non-animated thumbnail then we will generate our own, we can't depend on the server
         // because 1. encryption and 2. we can't ask the server specifically for a non-animated thumbnail.
         if (isAnimated && !SettingsStore.getValue("autoplayGifs")) {
-            if (!thumbUrl || !content?.info.thumbnail_info || mayBeAnimated(content.info.thumbnail_info.mimetype)) {
+            if (!thumbUrl || !content?.info?.thumbnail_info || mayBeAnimated(content.info.thumbnail_info.mimetype)) {
                 const img = document.createElement("img");
                 const loadPromise = new Promise((resolve, reject) => {
                     img.onload = resolve;
@@ -364,7 +366,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
         }
     }
 
-    protected getBanner(content: IMediaEventContent): JSX.Element {
+    protected getBanner(content: IMediaEventContent): ReactNode {
         // Hide it for the threads list & the file panel where we show it as text anyway.
         if (
             [TimelineRenderingType.ThreadsList, TimelineRenderingType.File].includes(this.context.timelineRenderingType)
@@ -429,9 +431,9 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
             forcedHeight ?? this.props.maxImageHeight,
         );
 
-        let img: JSX.Element;
-        let placeholder: JSX.Element;
-        let gifLabel: JSX.Element;
+        let img: JSX.Element | undefined;
+        let placeholder: JSX.Element | undefined;
+        let gifLabel: JSX.Element | undefined;
 
         if (!this.props.forExport && !this.state.imgLoaded) {
             const classes = classNames("mx_MImageBody_placeholder", {
@@ -471,7 +473,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
             gifLabel = <p className="mx_MImageBody_gifLabel">GIF</p>;
         }
 
-        let banner: JSX.Element;
+        let banner: ReactNode | undefined;
         if (this.state.showImage && this.state.hover) {
             banner = this.getBanner(content);
         }
@@ -526,7 +528,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
     }
 
     // Overridden by MStickerBody
-    protected getPlaceholder(width: number, height: number): JSX.Element {
+    protected getPlaceholder(width: number, height: number): ReactNode {
         const blurhash = this.props.mxEvent.getContent().info?.[BLURHASH_FIELD];
 
         if (blurhash) {
@@ -540,12 +542,12 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
     }
 
     // Overridden by MStickerBody
-    protected getTooltip(): JSX.Element {
+    protected getTooltip(): ReactNode {
         return null;
     }
 
     // Overridden by MStickerBody
-    protected getFileBody(): string | JSX.Element {
+    protected getFileBody(): ReactNode {
         if (this.props.forExport) return null;
         /*
          * In the room timeline or the thread context we don't need the download
@@ -577,7 +579,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
         }
 
         let contentUrl = this.state.contentUrl;
-        let thumbUrl: string;
+        let thumbUrl: string | undefined;
         if (this.props.forExport) {
             contentUrl = this.props.mxEvent.getContent().url ?? this.props.mxEvent.getContent().file?.url;
             thumbUrl = contentUrl;
diff --git a/src/components/views/messages/MJitsiWidgetEvent.tsx b/src/components/views/messages/MJitsiWidgetEvent.tsx
index 9eba08054e..50b180beed 100644
--- a/src/components/views/messages/MJitsiWidgetEvent.tsx
+++ b/src/components/views/messages/MJitsiWidgetEvent.tsx
@@ -41,7 +41,7 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
         const widgetId = this.props.mxEvent.getStateKey();
         const widget = WidgetStore.instance.getRoom(room.roomId, true).widgets.find((w) => w.id === widgetId);
 
-        let joinCopy = _t("Join the conference at the top of this room");
+        let joinCopy: string | null = _t("Join the conference at the top of this room");
         if (widget && WidgetLayoutStore.instance.isInContainer(room, widget, Container.Right)) {
             joinCopy = _t("Join the conference from the room information card on the right");
         } else if (!widget) {
diff --git a/src/components/views/messages/MKeyVerificationRequest.tsx b/src/components/views/messages/MKeyVerificationRequest.tsx
index 085ec60d72..8647a824ec 100644
--- a/src/components/views/messages/MKeyVerificationRequest.tsx
+++ b/src/components/views/messages/MKeyVerificationRequest.tsx
@@ -124,7 +124,7 @@ export default class MKeyVerificationRequest extends React.Component<IProps> {
 
         let title: string;
         let subtitle: string;
-        let stateNode: JSX.Element;
+        let stateNode: JSX.Element | undefined;
 
         if (!request.canAccept) {
             let stateLabel;
diff --git a/src/components/views/messages/MLocationBody.tsx b/src/components/views/messages/MLocationBody.tsx
index 3098ea2d5b..2932d361b8 100644
--- a/src/components/views/messages/MLocationBody.tsx
+++ b/src/components/views/messages/MLocationBody.tsx
@@ -37,7 +37,7 @@ import { IBodyProps } from "./IBodyProps";
 import { createReconnectedListener } from "../../../utils/connection";
 
 interface IState {
-    error: Error;
+    error?: Error;
 }
 
 export default class MLocationBody extends React.Component<IBodyProps, IState> {
@@ -58,9 +58,7 @@ export default class MLocationBody extends React.Component<IBodyProps, IState> {
 
         this.reconnectedListener = createReconnectedListener(this.clearError);
 
-        this.state = {
-            error: undefined,
-        };
+        this.state = {};
     }
 
     private onClick = (): void => {
@@ -149,7 +147,12 @@ export const LocationBodyContent: React.FC<LocationBodyContentProps> = ({
     const mapElement = (
         <Map id={mapId} centerGeoUri={geoUri} onClick={onClick} onError={onError} className="mx_MLocationBody_map">
             {({ map }) => (
-                <SmartMarker map={map} id={`${mapId}-marker`} geoUri={geoUri} roomMember={markerRoomMember} />
+                <SmartMarker
+                    map={map}
+                    id={`${mapId}-marker`}
+                    geoUri={geoUri}
+                    roomMember={markerRoomMember ?? undefined}
+                />
             )}
         </Map>
     );
diff --git a/src/components/views/messages/MPollBody.tsx b/src/components/views/messages/MPollBody.tsx
index 62c53af518..e65a8f67e6 100644
--- a/src/components/views/messages/MPollBody.tsx
+++ b/src/components/views/messages/MPollBody.tsx
@@ -128,7 +128,7 @@ export function launchPollEditor(mxEvent: MatrixEvent, getRelationsForEvent?: Ge
             PollCreateDialog,
             {
                 room: MatrixClientPeg.get().getRoom(mxEvent.getRoomId()),
-                threadId: mxEvent.getThread()?.id ?? null,
+                threadId: mxEvent.getThread()?.id,
                 editingMxEvent: mxEvent,
             },
             "mx_CompoundDialog",
diff --git a/src/components/views/messages/MStickerBody.tsx b/src/components/views/messages/MStickerBody.tsx
index 2aa5e16f80..4030b8e932 100644
--- a/src/components/views/messages/MStickerBody.tsx
+++ b/src/components/views/messages/MStickerBody.tsx
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React from "react";
+import React, { ReactNode } from "react";
 
 import MImageBody from "./MImageBody";
 import { BLURHASH_FIELD } from "../../../utils/image-media";
@@ -33,7 +33,7 @@ export default class MStickerBody extends MImageBody {
     // MStickerBody doesn't need a wrapping `<a href=...>`, but it does need extra padding
     // which is added by mx_MStickerBody_wrapper
     protected wrapImage(contentUrl: string, children: React.ReactNode): JSX.Element {
-        let onClick = null;
+        let onClick: React.MouseEventHandler | undefined;
         if (!this.state.showImage) {
             onClick = this.onClick;
         }
@@ -46,7 +46,7 @@ export default class MStickerBody extends MImageBody {
     }
 
     // Placeholder to show in place of the sticker image if img onLoad hasn't fired yet.
-    protected getPlaceholder(width: number, height: number): JSX.Element {
+    protected getPlaceholder(width: number, height: number): ReactNode {
         if (this.props.mxEvent.getContent().info?.[BLURHASH_FIELD]) return super.getPlaceholder(width, height);
         return (
             <img
@@ -61,7 +61,7 @@ export default class MStickerBody extends MImageBody {
     }
 
     // Tooltip to show on mouse over
-    protected getTooltip(): JSX.Element {
+    protected getTooltip(): ReactNode {
         const content = this.props.mxEvent && this.props.mxEvent.getContent();
 
         if (!content || !content.body || !content.info || !content.info.w) return null;
@@ -74,11 +74,11 @@ export default class MStickerBody extends MImageBody {
     }
 
     // Don't show "Download this_file.png ..."
-    protected getFileBody(): JSX.Element {
+    protected getFileBody(): ReactNode {
         return null;
     }
 
-    protected getBanner(content: IMediaEventContent): JSX.Element {
+    protected getBanner(content: IMediaEventContent): ReactNode {
         return null; // we don't need a banner, we have a tooltip
     }
 }
diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx
index 50623e2604..a7e60dfd5a 100644
--- a/src/components/views/messages/MVideoBody.tsx
+++ b/src/components/views/messages/MVideoBody.tsx
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React from "react";
+import React, { ReactNode } from "react";
 import { decode } from "blurhash";
 import { logger } from "matrix-js-sdk/src/logger";
 
@@ -31,13 +31,13 @@ import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContex
 import MediaProcessingError from "./shared/MediaProcessingError";
 
 interface IState {
-    decryptedUrl?: string;
-    decryptedThumbnailUrl?: string;
-    decryptedBlob?: Blob;
+    decryptedUrl: string | null;
+    decryptedThumbnailUrl: string | null;
+    decryptedBlob: Blob | null;
     error?: any;
     fetchingData: boolean;
     posterLoading: boolean;
-    blurhashUrl: string;
+    blurhashUrl: string | null;
 }
 
 export default class MVideoBody extends React.PureComponent<IBodyProps, IState> {
@@ -61,21 +61,21 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
         };
     }
 
-    private getContentUrl(): string | null {
+    private getContentUrl(): string | undefined {
         const content = this.props.mxEvent.getContent<IMediaEventContent>();
         // During export, the content url will point to the MSC, which will later point to a local url
-        if (this.props.forExport) return content.file?.url || content.url;
+        if (this.props.forExport) return content.file?.url ?? content.url;
         const media = mediaFromContent(content);
         if (media.isEncrypted) {
-            return this.state.decryptedUrl;
+            return this.state.decryptedUrl ?? undefined;
         } else {
-            return media.srcHttp;
+            return media.srcHttp ?? undefined;
         }
     }
 
     private hasContentUrl(): boolean {
         const url = this.getContentUrl();
-        return url && !url.startsWith("data:");
+        return !!url && !url.startsWith("data:");
     }
 
     private getThumbUrl(): string | null {
@@ -227,7 +227,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
         );
     }
 
-    private getFileBody = (): JSX.Element => {
+    private getFileBody = (): ReactNode => {
         if (this.props.forExport) return null;
         return this.showFileBody && <MFileBody {...this.props} showGenericPlaceholder={false} />;
     };
@@ -271,7 +271,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
 
         const contentUrl = this.getContentUrl();
         const thumbUrl = this.getThumbUrl();
-        let poster = null;
+        let poster: string | undefined;
         let preload = "metadata";
         if (content.info && thumbUrl) {
             poster = thumbUrl;
diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx
index 1a7d7732a3..6ac1e74586 100644
--- a/src/components/views/messages/MessageActionBar.tsx
+++ b/src/components/views/messages/MessageActionBar.tsx
@@ -63,7 +63,7 @@ interface IOptionsButtonProps {
     mxEvent: MatrixEvent;
     // TODO: Types
     getTile: () => any | null;
-    getReplyChain: () => ReplyChain;
+    getReplyChain: () => ReplyChain | null;
     permalinkCreator: RoomPermalinkCreator;
     onFocusChange: (menuDisplayed: boolean) => void;
     getRelationsForEvent?: GetRelationsForEvent;
@@ -97,10 +97,10 @@ const OptionsButton: React.FC<IOptionsButtonProps> = ({
         [openMenu, onFocus],
     );
 
-    let contextMenu: ReactElement | null;
-    if (menuDisplayed) {
+    let contextMenu: ReactElement | undefined;
+    if (menuDisplayed && button.current) {
         const tile = getTile && getTile();
-        const replyChain = getReplyChain && getReplyChain();
+        const replyChain = getReplyChain();
 
         const buttonRect = button.current.getBoundingClientRect();
         contextMenu = (
@@ -109,7 +109,7 @@ const OptionsButton: React.FC<IOptionsButtonProps> = ({
                 mxEvent={mxEvent}
                 permalinkCreator={permalinkCreator}
                 eventTileOps={tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined}
-                collapseReplyChain={replyChain && replyChain.canCollapse() ? replyChain.collapse : undefined}
+                collapseReplyChain={replyChain?.canCollapse() ? replyChain.collapse : undefined}
                 onFinished={closeMenu}
                 getRelationsForEvent={getRelationsForEvent}
             />
@@ -148,8 +148,8 @@ const ReactButton: React.FC<IReactButtonProps> = ({ mxEvent, reactions, onFocusC
         onFocusChange(menuDisplayed);
     }, [onFocusChange, menuDisplayed]);
 
-    let contextMenu;
-    if (menuDisplayed) {
+    let contextMenu: JSX.Element | undefined;
+    if (menuDisplayed && button.current) {
         const buttonRect = button.current.getBoundingClientRect();
         contextMenu = (
             <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} managed={false}>
@@ -211,7 +211,7 @@ const ReplyInThreadButton: React.FC<IReplyInThreadButton> = ({ mxEvent }) => {
         if (mxEvent.getThread() && !mxEvent.isThreadRoot) {
             defaultDispatcher.dispatch<ShowThreadPayload>({
                 action: Action.ShowThread,
-                rootEvent: mxEvent.getThread().rootEvent,
+                rootEvent: mxEvent.getThread()!.rootEvent,
                 initialEvent: mxEvent,
                 scroll_into_view: true,
                 highlighted: true,
@@ -293,7 +293,7 @@ interface IMessageActionBarProps {
     reactions?: Relations | null | undefined;
     // TODO: Types
     getTile: () => any | null;
-    getReplyChain: () => ReplyChain | undefined;
+    getReplyChain: () => ReplyChain | null;
     permalinkCreator?: RoomPermalinkCreator;
     onFocusChange?: (menuDisplayed: boolean) => void;
     toggleThreadExpanded: () => void;
@@ -421,7 +421,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
     };
 
     public render(): React.ReactNode {
-        const toolbarOpts = [];
+        const toolbarOpts: JSX.Element[] = [];
         if (canEditContent(this.props.mxEvent)) {
             toolbarOpts.push(
                 <RovingAccessibleTooltipButton
@@ -452,8 +452,8 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
 
         // We show a different toolbar for failed events, so detect that first.
         const mxEvent = this.props.mxEvent;
-        const editStatus = mxEvent.replacingEvent() && mxEvent.replacingEvent().status;
-        const redactStatus = mxEvent.localRedactionEvent() && mxEvent.localRedactionEvent().status;
+        const editStatus = mxEvent.replacingEvent()?.status;
+        const redactStatus = mxEvent.localRedactionEvent()?.status;
         const allowCancel = canCancel(mxEvent.status) || canCancel(editStatus) || canCancel(redactStatus);
         const isFailed = [mxEvent.status, editStatus, redactStatus].includes(EventStatus.NOT_SENT);
         if (allowCancel && isFailed) {
diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx
index a32747cd2e..7e2a8b8db9 100644
--- a/src/components/views/messages/MessageEvent.tsx
+++ b/src/components/views/messages/MessageEvent.tsx
@@ -58,7 +58,7 @@ interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper
 }
 
 export interface IOperableEventTile {
-    getEventTileOps(): IEventTileOps;
+    getEventTileOps(): IEventTileOps | null;
 }
 
 const baseBodyTypes = new Map<string, typeof React.Component>([
@@ -159,12 +159,12 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
             if (this.props.mxEvent.isDecryptionFailure()) {
                 BodyType = DecryptionFailureBody;
             } else if (type && this.evTypes.has(type)) {
-                BodyType = this.evTypes.get(type);
+                BodyType = this.evTypes.get(type)!;
             } else if (msgtype && this.bodyTypes.has(msgtype)) {
-                BodyType = this.bodyTypes.get(msgtype);
+                BodyType = this.bodyTypes.get(msgtype)!;
             } else if (content.url) {
                 // Fallback to MFileBody if there's a content URL
-                BodyType = this.bodyTypes.get(MsgType.File);
+                BodyType = this.bodyTypes.get(MsgType.File)!;
             } else {
                 // Fallback to UnknownBody otherwise if not redacted
                 BodyType = UnknownBody;
diff --git a/src/components/views/messages/ReactionsRow.tsx b/src/components/views/messages/ReactionsRow.tsx
index 39cee90977..6d7b0b2d17 100644
--- a/src/components/views/messages/ReactionsRow.tsx
+++ b/src/components/views/messages/ReactionsRow.tsx
@@ -142,7 +142,7 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
         this.forceUpdate();
     };
 
-    private getMyReactions(): MatrixEvent[] {
+    private getMyReactions(): MatrixEvent[] | null {
         const reactions = this.props.reactions;
         if (!reactions) {
             return null;
@@ -206,7 +206,7 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
         // Show the first MAX_ITEMS if there are MAX_ITEMS + 1 or more items.
         // The "+ 1" ensure that the "show all" reveals something that takes up
         // more space than the button itself.
-        let showAllButton: JSX.Element;
+        let showAllButton: JSX.Element | undefined;
         if (items.length > MAX_ITEMS_WHEN_LIMITED + 1 && !showAll) {
             items = items.slice(0, MAX_ITEMS_WHEN_LIMITED);
             showAllButton = (
diff --git a/src/components/views/messages/ReactionsRowButton.tsx b/src/components/views/messages/ReactionsRowButton.tsx
index 38c1ca55e5..60bad283b7 100644
--- a/src/components/views/messages/ReactionsRowButton.tsx
+++ b/src/components/views/messages/ReactionsRowButton.tsx
@@ -106,9 +106,9 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
         }
 
         const room = this.context.getRoom(mxEvent.getRoomId());
-        let label: string;
+        let label: string | undefined;
         if (room) {
-            const senders = [];
+            const senders: string[] = [];
             for (const reactionEvent of reactionEvents) {
                 const member = room.getMember(reactionEvent.getSender());
                 senders.push(member?.name || reactionEvent.getSender());
diff --git a/src/components/views/messages/SenderProfile.tsx b/src/components/views/messages/SenderProfile.tsx
index 4027c1ded4..ae82f40404 100644
--- a/src/components/views/messages/SenderProfile.tsx
+++ b/src/components/views/messages/SenderProfile.tsx
@@ -43,5 +43,7 @@ export default function SenderProfile({ mxEvent, onClick, withTooltip }: IProps)
             emphasizeDisplayName={true}
             withTooltip={withTooltip}
         />
-    ) : null;
+    ) : (
+        <></>
+    );
 }
diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx
index dc7cc4dc23..05f6da046d 100644
--- a/src/components/views/messages/TextualBody.tsx
+++ b/src/components/views/messages/TextualBody.tsx
@@ -342,7 +342,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
             if (node.tagName === "SPAN" && typeof node.getAttribute("data-mx-spoiler") === "string") {
                 const spoilerContainer = document.createElement("span");
 
-                const reason = node.getAttribute("data-mx-spoiler");
+                const reason = node.getAttribute("data-mx-spoiler") ?? undefined;
                 node.removeAttribute("data-mx-spoiler"); // we don't want to recurse
                 const spoiler = <Spoiler reason={reason} contentHtml={node.outerHTML} />;
 
@@ -367,7 +367,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
             const node = nodes[i];
             if (node.tagName === "A" && node.getAttribute("href")) {
                 if (this.isLinkPreviewable(node)) {
-                    links.push(node.getAttribute("href"));
+                    links.push(node.getAttribute("href")!);
                 }
             } else if (node.tagName === "PRE" || node.tagName === "CODE" || node.tagName === "BLOCKQUOTE") {
                 continue;
@@ -380,7 +380,8 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
 
     private isLinkPreviewable(node: Element): boolean {
         // don't try to preview relative links
-        if (!node.getAttribute("href").startsWith("http://") && !node.getAttribute("href").startsWith("https://")) {
+        const href = node.getAttribute("href") ?? "";
+        if (!href.startsWith("http://") && !href.startsWith("https://")) {
             return false;
         }
 
@@ -389,24 +390,24 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
         // or from a full foo.bar/baz style schemeless URL) - or be a markdown-style
         // link, in which case we check the target text differs from the link value.
         // TODO: make this configurable?
-        if (node.textContent.indexOf("/") > -1) {
+        if (node.textContent?.includes("/")) {
             return true;
+        }
+
+        const url = node.getAttribute("href");
+        const host = url.match(/^https?:\/\/(.*?)(\/|$)/)[1];
+
+        // never preview permalinks (if anything we should give a smart
+        // preview of the room/user they point to: nobody needs to be reminded
+        // what the matrix.to site looks like).
+        if (isPermalinkHost(host)) return false;
+
+        if (node.textContent?.toLowerCase().trim().startsWith(host.toLowerCase())) {
+            // it's a "foo.pl" style link
+            return false;
         } else {
-            const url = node.getAttribute("href");
-            const host = url.match(/^https?:\/\/(.*?)(\/|$)/)[1];
-
-            // never preview permalinks (if anything we should give a smart
-            // preview of the room/user they point to: nobody needs to be reminded
-            // what the matrix.to site looks like).
-            if (isPermalinkHost(host)) return false;
-
-            if (node.textContent.toLowerCase().trim().startsWith(host.toLowerCase())) {
-                // it's a "foo.pl" style link
-                return false;
-            } else {
-                // it's a [foo bar](http://foo.com) style link
-                return true;
-            }
+            // it's a [foo bar](http://foo.com) style link
+            return true;
         }
     }
 
@@ -434,7 +435,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
      * to start with (e.g. pills, links in the content).
      */
     private onBodyLinkClick = (e: MouseEvent): void => {
-        let target = e.target as HTMLLinkElement;
+        let target: HTMLLinkElement | null = e.target as HTMLLinkElement;
         // links processed by linkifyjs have their own handler so don't handle those here
         if (target.classList.contains(linkifyOpts.className as string)) return;
         if (target.nodeName !== "A") {
diff --git a/src/components/views/messages/TileErrorBoundary.tsx b/src/components/views/messages/TileErrorBoundary.tsx
index 220a1f0697..d87798a8e6 100644
--- a/src/components/views/messages/TileErrorBoundary.tsx
+++ b/src/components/views/messages/TileErrorBoundary.tsx
@@ -34,16 +34,14 @@ interface IProps {
 }
 
 interface IState {
-    error: Error;
+    error?: Error;
 }
 
 export default class TileErrorBoundary extends React.Component<IProps, IState> {
     public constructor(props: IProps) {
         super(props);
 
-        this.state = {
-            error: null,
-        };
+        this.state = {};
     }
 
     public static getDerivedStateFromError(error: Error): Partial<IState> {
diff --git a/src/components/views/rooms/Autocomplete.tsx b/src/components/views/rooms/Autocomplete.tsx
index ba26bf3a87..daf96fd508 100644
--- a/src/components/views/rooms/Autocomplete.tsx
+++ b/src/components/views/rooms/Autocomplete.tsx
@@ -117,7 +117,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
                 // Hide the autocomplete box
                 hide: true,
             });
-            return Promise.resolve(null);
+            return Promise.resolve();
         }
         let autocompleteDelay = SettingsStore.getValue("autocompleteDelay");
 
@@ -204,7 +204,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
         this.setSelection(1 + index);
     }
 
-    public onEscape(e: KeyboardEvent): boolean {
+    public onEscape(e: KeyboardEvent): boolean | undefined {
         const completionCount = this.countCompletions();
         if (completionCount === 0) {
             // autocomplete is already empty, so don't preventDefault
diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx
index b94923db3a..d251285816 100644
--- a/src/components/views/rooms/BasicMessageComposer.tsx
+++ b/src/components/views/rooms/BasicMessageComposer.tsx
@@ -132,7 +132,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
 
     private _isCaretAtEnd: boolean;
     private lastCaret: DocumentOffset;
-    private lastSelection: ReturnType<typeof cloneSelection>;
+    private lastSelection: ReturnType<typeof cloneSelection> | null;
 
     private readonly useMarkdownHandle: string;
     private readonly emoticonSettingHandle: string;
@@ -188,7 +188,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
         }
     }
 
-    public replaceEmoticon(caretPosition: DocumentPosition, regex: RegExp): number {
+    public replaceEmoticon(caretPosition: DocumentPosition, regex: RegExp): number | undefined {
         const { model } = this.props;
         const range = model.startRange(caretPosition);
         // expand range max 9 characters backwards from caretPosition,
@@ -352,7 +352,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
         this.onCutCopy(event, "cut");
     };
 
-    private onPaste = (event: ClipboardEvent<HTMLDivElement>): boolean => {
+    private onPaste = (event: ClipboardEvent<HTMLDivElement>): boolean | undefined => {
         event.preventDefault(); // we always handle the paste ourselves
         if (this.props.onPaste?.(event, this.props.model)) {
             // to prevent double handling, allow props.onPaste to skip internal onPaste
@@ -415,7 +415,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
         this.lastSelection = cloneSelection(document.getSelection());
     }
 
-    private refreshLastCaretIfNeeded(): DocumentOffset {
+    private refreshLastCaretIfNeeded(): DocumentOffset | undefined {
         // XXX: needed when going up and down in editing messages ... not sure why yet
         // because the editors should stop doing this when when blurred ...
         // maybe it's on focus and the _editorRef isn't available yet or something.
@@ -441,7 +441,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
     }
 
     public isSelectionCollapsed(): boolean {
-        return !this.lastSelection || this.lastSelection.isCollapsed;
+        return !this.lastSelection || !!this.lastSelection.isCollapsed;
     }
 
     public isCaretAtStart(): boolean {
@@ -537,7 +537,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
             // there is no current autocomplete window, try to open it
             this.tabCompleteName();
             handled = true;
-        } else if ([KeyBindingAction.Delete, KeyBindingAction.Backspace].includes(accessibilityAction)) {
+        } else if ([KeyBindingAction.Delete, KeyBindingAction.Backspace].includes(accessibilityAction!)) {
             this.formatBarRef.current.hide();
         }
 
@@ -750,7 +750,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
     };
 
     public render(): React.ReactNode {
-        let autoComplete;
+        let autoComplete: JSX.Element | undefined;
         if (this.state.autoComplete) {
             const query = this.state.query;
             const queryLen = query.length;
@@ -785,7 +785,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
 
         const { completionIndex } = this.state;
         const hasAutocomplete = Boolean(this.state.autoComplete);
-        let activeDescendant: string;
+        let activeDescendant: string | undefined;
         if (hasAutocomplete && completionIndex >= 0) {
             activeDescendant = generateCompletionDomId(completionIndex);
         }
@@ -800,7 +800,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
                 />
                 <div
                     className={classes}
-                    contentEditable={this.props.disabled ? null : true}
+                    contentEditable={this.props.disabled ? undefined : true}
                     tabIndex={0}
                     onBlur={this.onBlur}
                     onFocus={this.onFocus}
diff --git a/src/components/views/rooms/E2EIcon.tsx b/src/components/views/rooms/E2EIcon.tsx
index b93f87abc5..0103c308dd 100644
--- a/src/components/views/rooms/E2EIcon.tsx
+++ b/src/components/views/rooms/E2EIcon.tsx
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React, { useState } from "react";
+import React, { CSSProperties, useState } from "react";
 import classNames from "classnames";
 
 import { _t, _td } from "../../../languageHandler";
@@ -44,7 +44,7 @@ const crossSigningRoomTitles: { [key in E2EState]?: string } = {
 
 interface IProps {
     isUser?: boolean;
-    status: E2EState | E2EStatus;
+    status?: E2EState | E2EStatus;
     className?: string;
     size?: number;
     onClick?: () => void;
@@ -77,13 +77,15 @@ const E2EIcon: React.FC<IProps> = ({
     );
 
     let e2eTitle: string | undefined;
-    if (isUser) {
-        e2eTitle = crossSigningUserTitles[status];
-    } else {
-        e2eTitle = crossSigningRoomTitles[status];
+    if (status) {
+        if (isUser) {
+            e2eTitle = crossSigningUserTitles[status];
+        } else {
+            e2eTitle = crossSigningRoomTitles[status];
+        }
     }
 
-    let style;
+    let style: CSSProperties | undefined;
     if (size) {
         style = { width: `${size}px`, height: `${size}px` };
     }
@@ -91,7 +93,7 @@ const E2EIcon: React.FC<IProps> = ({
     const onMouseOver = (): void => setHover(true);
     const onMouseLeave = (): void => setHover(false);
 
-    let tip;
+    let tip: JSX.Element | undefined;
     if (hover && !hideTooltip) {
         tip = <Tooltip label={e2eTitle ? _t(e2eTitle) : ""} alignment={tooltipAlignment} />;
     }
diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx
index 066d146f13..9d21284c27 100644
--- a/src/components/views/rooms/EditMessageComposer.tsx
+++ b/src/components/views/rooms/EditMessageComposer.tsx
@@ -47,6 +47,7 @@ import { getSlashCommand, isSlashCommand, runSlashCommand, shouldSendAnyway } fr
 import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
 import { PosthogAnalytics } from "../../../PosthogAnalytics";
 import { editorRoomKey, editorStateKey } from "../../../Editing";
+import DocumentOffset from "../../../editor/offset";
 
 function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
     const html = mxEvent.getContent().formatted_body;
@@ -130,7 +131,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
 
     private readonly editorRef = createRef<BasicMessageComposer>();
     private readonly dispatcherRef: string;
-    private model: EditorModel = null;
+    private model: EditorModel;
 
     public constructor(props: IEditMessageComposerProps, context: React.ContextType<typeof RoomContext>) {
         super(props);
@@ -250,7 +251,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
         return localStorage.getItem(this.editorRoomKey) !== null;
     }
 
-    private restoreStoredEditorState(partCreator: PartCreator): Part[] {
+    private restoreStoredEditorState(partCreator: PartCreator): Part[] | undefined {
         const json = localStorage.getItem(this.editorStateKey);
         if (json) {
             try {
@@ -382,7 +383,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
         // editorstate so it can be restored when the remote echo event tile gets rendered
         // in case we're currently editing a pending event
         const sel = document.getSelection();
-        let caret;
+        let caret: DocumentOffset | undefined;
         if (sel.focusNode) {
             caret = getCaretOffsetAndText(this.editorRef.current?.editorRef.current, sel).caret;
         }
@@ -390,7 +391,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
         // if caret is undefined because for some reason there isn't a valid selection,
         // then when mounting the editor again with the same editor state,
         // it will set the cursor at the end.
-        this.props.editState.setEditorState(caret, parts);
+        this.props.editState.setEditorState(caret ?? null, parts);
         window.removeEventListener("beforeunload", this.saveStoredEditorState);
         if (this.shouldSaveStoredEditorState) {
             this.saveStoredEditorState();
@@ -462,7 +463,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
                     model={this.model}
                     room={this.getRoom()}
                     threadId={this.props.editState?.getEvent()?.getThread()?.id}
-                    initialCaret={this.props.editState.getCaret()}
+                    initialCaret={this.props.editState.getCaret() ?? undefined}
                     label={_t("Edit message")}
                     onChange={this.onChange}
                 />
diff --git a/src/components/views/rooms/EntityTile.tsx b/src/components/views/rooms/EntityTile.tsx
index 14d4bbc45a..16523918aa 100644
--- a/src/components/views/rooms/EntityTile.tsx
+++ b/src/components/views/rooms/EntityTile.tsx
@@ -69,14 +69,14 @@ interface IProps {
     title?: string;
     avatarJsx?: JSX.Element; // <BaseAvatar />
     className?: string;
-    presenceState?: PresenceState;
-    presenceLastActiveAgo?: number;
-    presenceLastTs?: number;
+    presenceState: PresenceState;
+    presenceLastActiveAgo: number;
+    presenceLastTs: number;
     presenceCurrentlyActive?: boolean;
-    showInviteButton?: boolean;
+    showInviteButton: boolean;
     onClick(): void;
-    suppressOnHover?: boolean;
-    showPresence?: boolean;
+    suppressOnHover: boolean;
+    showPresence: boolean;
     subtextLabel?: string;
     e2eStatus?: E2EState;
     powerStatus?: PowerStatus;
diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx
index c416c9005d..d8e02f2d9a 100644
--- a/src/components/views/rooms/EventTile.tsx
+++ b/src/components/views/rooms/EventTile.tsx
@@ -274,8 +274,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
             verified: null,
             // The Relations model from the JS SDK for reactions to `mxEvent`
             reactions: this.getReactions(),
-            // Context menu position
-            contextMenu: null,
 
             hover: false,
 
@@ -697,13 +695,13 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
         });
     };
 
-    private renderE2EPadlock(): JSX.Element {
+    private renderE2EPadlock(): ReactNode {
         // if the event was edited, show the verification info for the edit, not
         // the original
         const ev = this.props.mxEvent.replacingEvent() ?? this.props.mxEvent;
 
         // no icon for local rooms
-        if (isLocalRoom(ev.getRoomId()!)) return;
+        if (isLocalRoom(ev.getRoomId()!)) return null;
 
         // event could not be decrypted
         if (ev.isDecryptionFailure()) {
@@ -713,9 +711,9 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
         // event is encrypted and not redacted, display padlock corresponding to whether or not it is verified
         if (ev.isEncrypted() && !ev.isRedacted()) {
             if (this.state.verified === E2EState.Normal) {
-                return; // no icon if we've not even cross-signed the user
+                return null; // no icon if we've not even cross-signed the user
             } else if (this.state.verified === E2EState.Verified) {
-                return; // no icon for verified
+                return null; // no icon for verified
             } else if (this.state.verified === E2EState.Unauthenticated) {
                 return <E2ePadlockUnauthenticated />;
             } else if (this.state.verified === E2EState.Unknown) {
@@ -729,16 +727,16 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
             // else if room is encrypted
             // and event is being encrypted or is not_sent (Unknown Devices/Network Error)
             if (ev.status === EventStatus.ENCRYPTING) {
-                return;
+                return null;
             }
             if (ev.status === EventStatus.NOT_SENT) {
-                return;
+                return null;
             }
             if (ev.isState()) {
-                return; // we expect this to be unencrypted
+                return null; // we expect this to be unencrypted
             }
             if (ev.isRedacted()) {
-                return; // we expect this to be unencrypted
+                return null; // we expect this to be unencrypted
             }
             // if the event is not encrypted, but it's an e2e room, show the open padlock
             return <E2ePadlockUnencrypted />;
@@ -752,16 +750,16 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
         this.setState({ actionBarFocused });
     };
 
-    private getTile: () => IEventTileType = () => this.tile.current;
+    private getTile: () => IEventTileType | null = () => this.tile.current;
 
-    private getReplyChain = (): ReplyChain => this.replyChain.current;
+    private getReplyChain = (): ReplyChain | null => this.replyChain.current;
 
-    private getReactions = (): Relations => {
+    private getReactions = (): Relations | null => {
         if (!this.props.showReactions || !this.props.getRelationsForEvent) {
             return null;
         }
         const eventId = this.props.mxEvent.getId();
-        return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction");
+        return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction") ?? null;
     };
 
     private onReactionsCreated = (relationType: string, eventType: string): void => {
@@ -795,7 +793,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
         // Return if we're in a browser and click either an a tag or we have
         // selected text, as in those cases we want to use the native browser
         // menu
-        if (!PlatformPeg.get().allowOverridingNativeContextMenus() && (getSelectedText() || anchorElement)) return;
+        if (!PlatformPeg.get()?.allowOverridingNativeContextMenus() && (getSelectedText() || anchorElement)) return;
 
         // We don't want to show the menu when editing a message
         if (this.props.editState) return;
@@ -817,7 +815,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
 
     private onCloseMenu = (): void => {
         this.setState({
-            contextMenu: null,
+            contextMenu: undefined,
             actionBarFocused: false,
         });
     };
@@ -901,7 +899,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
                 this.props.mxEvent.getContent().msgtype === MsgType.Emote,
         });
 
-        const isSending = ["sending", "queued", "encrypting"].indexOf(this.props.eventSendStatus) !== -1;
+        const isSending = ["sending", "queued", "encrypting"].includes(this.props.eventSendStatus!);
         const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted;
         const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure();
 
@@ -1110,7 +1108,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
         const groupPadlock = !useIRCLayout && !isBubbleMessage && this.renderE2EPadlock();
         const ircPadlock = useIRCLayout && !isBubbleMessage && this.renderE2EPadlock();
 
-        let msgOption;
+        let msgOption: JSX.Element | undefined;
         if (this.props.showReadReceipts) {
             if (this.shouldShowSentReceipt || this.shouldShowSendingReceipt) {
                 msgOption = <SentReceipt messageState={this.props.mxEvent.getAssociatedStatus()} />;
@@ -1127,7 +1125,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
             }
         }
 
-        let replyChain;
+        let replyChain: JSX.Element | undefined;
         if (
             haveRendererForEvent(this.props.mxEvent, this.context.showHiddenEvents) &&
             shouldDisplayReply(this.props.mxEvent)
@@ -1480,7 +1478,7 @@ class E2ePadlock extends React.Component<IE2ePadlockProps, IE2ePadlockState> {
     };
 
     public render(): React.ReactNode {
-        let tooltip = null;
+        let tooltip: JSX.Element | undefined;
         if (this.state.hover) {
             tooltip = <Tooltip className="mx_EventTile_e2eIcon_tooltip" label={this.props.title} />;
         }
@@ -1506,7 +1504,7 @@ function SentReceipt({ messageState }: ISentReceiptProps): JSX.Element {
         mx_EventTile_receiptSending: !isSent && !isFailed,
     });
 
-    let nonCssBadge = null;
+    let nonCssBadge: JSX.Element | undefined;
     if (isFailed) {
         nonCssBadge = <NotificationBadge notification={StaticNotificationState.RED_EXCLAMATION} />;
     }
diff --git a/src/components/views/rooms/HistoryTile.tsx b/src/components/views/rooms/HistoryTile.tsx
index 6309f20366..c720a1b090 100644
--- a/src/components/views/rooms/HistoryTile.tsx
+++ b/src/components/views/rooms/HistoryTile.tsx
@@ -24,11 +24,11 @@ import { _t } from "../../../languageHandler";
 const HistoryTile: React.FC = () => {
     const { room } = useContext(RoomContext);
 
-    const oldState = room.getLiveTimeline().getState(EventTimeline.BACKWARDS);
-    const encryptionState = oldState.getStateEvents("m.room.encryption")[0];
-    const historyState = oldState.getStateEvents("m.room.history_visibility")[0]?.getContent().history_visibility;
+    const oldState = room?.getLiveTimeline().getState(EventTimeline.BACKWARDS);
+    const encryptionState = oldState?.getStateEvents("m.room.encryption")[0];
+    const historyState = oldState?.getStateEvents("m.room.history_visibility")[0]?.getContent().history_visibility;
 
-    let subtitle;
+    let subtitle: string | undefined;
     if (historyState == "invited") {
         subtitle = _t("You don't have permission to view messages from before you were invited.");
     } else if (historyState == "joined") {
diff --git a/src/components/views/rooms/LinkPreviewGroup.tsx b/src/components/views/rooms/LinkPreviewGroup.tsx
index ae32c6c166..e4a3df8c1d 100644
--- a/src/components/views/rooms/LinkPreviewGroup.tsx
+++ b/src/components/views/rooms/LinkPreviewGroup.tsx
@@ -54,7 +54,7 @@ const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onH
 
     const showPreviews = expanded ? previews : previews.slice(0, INITIAL_NUM_PREVIEWS);
 
-    let toggleButton: JSX.Element;
+    let toggleButton: JSX.Element | undefined;
     if (previews.length > INITIAL_NUM_PREVIEWS) {
         toggleButton = (
             <AccessibleButton onClick={toggleExpanded}>
@@ -94,7 +94,7 @@ const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onH
 
 const fetchPreviews = (cli: MatrixClient, links: string[], ts: number): Promise<[string, IPreviewUrlResponse][]> => {
     return Promise.all<[string, IPreviewUrlResponse] | void>(
-        links.map(async (link): Promise<[string, IPreviewUrlResponse]> => {
+        links.map(async (link): Promise<[string, IPreviewUrlResponse] | undefined> => {
             try {
                 const preview = await cli.getUrlPreview(link, ts);
                 if (preview && Object.keys(preview).length > 0) {
diff --git a/src/components/views/rooms/LinkPreviewWidget.tsx b/src/components/views/rooms/LinkPreviewWidget.tsx
index 9618b5cd00..0172a61549 100644
--- a/src/components/views/rooms/LinkPreviewWidget.tsx
+++ b/src/components/views/rooms/LinkPreviewWidget.tsx
@@ -44,7 +44,7 @@ export default class LinkPreviewWidget extends React.Component<IProps> {
         ev.preventDefault();
 
         let src = p["og:image"];
-        if (src && src.startsWith("mxc://")) {
+        if (src?.startsWith("mxc://")) {
             src = mediaFromMxc(src).srcHttp;
         }
 
@@ -68,7 +68,7 @@ export default class LinkPreviewWidget extends React.Component<IProps> {
             };
         }
 
-        Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
+        Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", undefined, true);
     };
 
     public render(): React.ReactNode {
@@ -78,7 +78,7 @@ export default class LinkPreviewWidget extends React.Component<IProps> {
         }
 
         // FIXME: do we want to factor out all image displaying between this and MImageBody - especially for lightboxing?
-        let image = p["og:image"];
+        let image: string | null = p["og:image"] ?? null;
         if (!SettingsStore.getValue("showImages")) {
             image = null; // Don't render a button to show the image, just hide it outright
         }
@@ -99,7 +99,7 @@ export default class LinkPreviewWidget extends React.Component<IProps> {
             );
         }
 
-        let img;
+        let img: JSX.Element | undefined;
         if (image) {
             img = (
                 <div className="mx_LinkPreviewWidget_image" style={{ height: thumbHeight }}>
diff --git a/src/components/views/rooms/MemberList.tsx b/src/components/views/rooms/MemberList.tsx
index 42811142d7..6c5a871a9a 100644
--- a/src/components/views/rooms/MemberList.tsx
+++ b/src/components/views/rooms/MemberList.tsx
@@ -123,7 +123,9 @@ export default class MemberList extends React.Component<IProps, IState> {
         const cli = MatrixClientPeg.get();
         const room = cli.getRoom(this.props.roomId);
 
-        return room?.canInvite(cli.getUserId()) || (room?.isSpaceRoom() && room.getJoinRule() === JoinRule.Public);
+        return (
+            !!room?.canInvite(cli.getSafeUserId()) || !!(room?.isSpaceRoom() && room.getJoinRule() === JoinRule.Public)
+        );
     }
 
     private getMembersState(invitedMembers: Array<RoomMember>, joinedMembers: Array<RoomMember>): IState {
@@ -276,7 +278,7 @@ export default class MemberList extends React.Component<IProps, IState> {
         });
     };
 
-    private getPending3PidInvites(): Array<MatrixEvent> {
+    private getPending3PidInvites(): MatrixEvent[] | undefined {
         // include 3pid invites (m.room.third_party_invite) state events.
         // The HS may have already converted these into m.room.member invites so
         // we shouldn't add them if the 3pid invite state key (token) is in the
diff --git a/src/components/views/rooms/MemberTile.tsx b/src/components/views/rooms/MemberTile.tsx
index d9fba0b029..cac793066b 100644
--- a/src/components/views/rooms/MemberTile.tsx
+++ b/src/components/views/rooms/MemberTile.tsx
@@ -41,7 +41,7 @@ interface IProps {
 
 interface IState {
     isRoomEncrypted: boolean;
-    e2eStatus: E2EState;
+    e2eStatus?: E2EState;
 }
 
 export default class MemberTile extends React.Component<IProps, IState> {
@@ -57,7 +57,6 @@ export default class MemberTile extends React.Component<IProps, IState> {
 
         this.state = {
             isRoomEncrypted: false,
-            e2eStatus: null,
         };
     }
 
@@ -187,7 +186,7 @@ export default class MemberTile extends React.Component<IProps, IState> {
     public render(): React.ReactNode {
         const member = this.props.member;
         const name = this.getDisplayName();
-        const presenceState = member.user?.presence ?? null;
+        const presenceState = member.user?.presence as PresenceState | undefined;
 
         const av = <MemberAvatar member={member} width={36} height={36} aria-hidden="true" />;
 
@@ -222,7 +221,7 @@ export default class MemberTile extends React.Component<IProps, IState> {
         return (
             <EntityTile
                 {...this.props}
-                presenceState={presenceState as PresenceState | null}
+                presenceState={presenceState}
                 presenceLastActiveAgo={member.user ? member.user.lastActiveAgo : 0}
                 presenceLastTs={member.user ? member.user.lastPresenceTs : 0}
                 presenceCurrentlyActive={member.user ? member.user.currentlyActive : false}
diff --git a/src/components/views/rooms/MessageComposerButtons.tsx b/src/components/views/rooms/MessageComposerButtons.tsx
index b5faf878a6..30856cabc7 100644
--- a/src/components/views/rooms/MessageComposerButtons.tsx
+++ b/src/components/views/rooms/MessageComposerButtons.tsx
@@ -172,7 +172,7 @@ export const UploadButtonContext = createContext<UploadButtonFn | null>(null);
 
 interface IUploadButtonProps {
     roomId: string;
-    relation?: IEventRelation | null;
+    relation?: IEventRelation;
     children: ReactNode;
 }
 
@@ -197,11 +197,11 @@ const UploadButtonContextProvider: React.FC<IUploadButtonProps> = ({ roomId, rel
     });
 
     const onUploadFileInputChange = (ev: React.ChangeEvent<HTMLInputElement>): void => {
-        if (ev.target.files.length === 0) return;
+        if (ev.target.files?.length === 0) return;
 
         // Take a copy, so we can safely reset the value of the form control
         ContentMessages.sharedInstance().sendContentListToRoom(
-            Array.from(ev.target.files),
+            Array.from(ev.target.files!),
             roomId,
             relation,
             cli,
@@ -316,7 +316,7 @@ class PollButton extends React.PureComponent<IPollButtonProps> {
             });
         } else {
             const threadId =
-                this.props.relation?.rel_type === THREAD_RELATION_TYPE.name ? this.props.relation.event_id : null;
+                this.props.relation?.rel_type === THREAD_RELATION_TYPE.name ? this.props.relation.event_id : undefined;
 
             Modal.createDialog(
                 PollCreateDialog,
diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx
index 070999b4fd..7f2aa3ce3f 100644
--- a/src/components/views/rooms/NewRoomIntro.tsx
+++ b/src/components/views/rooms/NewRoomIntro.tsx
@@ -166,7 +166,7 @@ const NewRoomIntro: React.FC = () => {
         const creator = room.currentState.getStateEvents(EventType.RoomCreate, "")?.getSender();
         const creatorName = room?.getMember(creator)?.rawDisplayName || creator;
 
-        let createdText;
+        let createdText: string;
         if (creator === cli.getUserId()) {
             createdText = _t("You created this room.");
         } else {
@@ -175,15 +175,15 @@ const NewRoomIntro: React.FC = () => {
             });
         }
 
-        let parentSpace: Room;
+        let parentSpace: Room | undefined;
         if (
-            SpaceStore.instance.activeSpaceRoom?.canInvite(cli.getUserId()) &&
+            SpaceStore.instance.activeSpaceRoom?.canInvite(cli.getSafeUserId()) &&
             SpaceStore.instance.isRoomInSpace(SpaceStore.instance.activeSpace, room.roomId)
         ) {
             parentSpace = SpaceStore.instance.activeSpaceRoom;
         }
 
-        let buttons;
+        let buttons: JSX.Element | undefined;
         if (parentSpace && shouldShowComponent(UIComponent.InviteUsers)) {
             buttons = (
                 <div className="mx_NewRoomIntro_buttons">
@@ -191,12 +191,12 @@ const NewRoomIntro: React.FC = () => {
                         className="mx_NewRoomIntro_inviteButton"
                         kind="primary"
                         onClick={() => {
-                            showSpaceInvite(parentSpace);
+                            showSpaceInvite(parentSpace!);
                         }}
                     >
                         {_t("Invite to %(spaceName)s", { spaceName: parentSpace.name })}
                     </AccessibleButton>
-                    {room.canInvite(cli.getUserId()) && (
+                    {room.canInvite(cli.getSafeUserId()) && (
                         <AccessibleButton
                             className="mx_NewRoomIntro_inviteButton"
                             kind="primary_outline"
@@ -209,7 +209,7 @@ const NewRoomIntro: React.FC = () => {
                     )}
                 </div>
             );
-        } else if (room.canInvite(cli.getUserId()) && shouldShowComponent(UIComponent.InviteUsers)) {
+        } else if (room.canInvite(cli.getSafeUserId()) && shouldShowComponent(UIComponent.InviteUsers)) {
             buttons = (
                 <div className="mx_NewRoomIntro_buttons">
                     <AccessibleButton
diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx
index 968a49b8ca..51480df1fc 100644
--- a/src/components/views/rooms/NotificationBadge.tsx
+++ b/src/components/views/rooms/NotificationBadge.tsx
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React, { MouseEvent } from "react";
+import React, { MouseEvent, ReactNode } from "react";
 
 import SettingsStore from "../../../settings/SettingsStore";
 import { XOR } from "../../../@types/common";
@@ -71,7 +71,7 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
         );
     }
 
-    private get roomId(): string {
+    private get roomId(): string | null {
         // We should convert this to null for safety with the SettingsStore
         return this.props.roomId || null;
     }
@@ -110,7 +110,7 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
         });
     };
 
-    public render(): React.ReactElement {
+    public render(): ReactNode {
         /* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
         const { notification, showUnsentTooltip, forceCount, onClick } = this.props;
 
@@ -119,8 +119,8 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
             if (!notification.hasUnreadCount) return null; // Can't render a badge
         }
 
-        let label: string;
-        let tooltip: JSX.Element;
+        let label: string | undefined;
+        let tooltip: JSX.Element | undefined;
         if (showUnsentTooltip && this.state.showTooltip && notification.color === NotificationColor.Unsent) {
             label = _t("Message didn't send. Click for info.");
             tooltip = <Tooltip className="mx_RoleButton_tooltip" label={label} />;
diff --git a/src/components/views/rooms/ReadReceiptGroup.tsx b/src/components/views/rooms/ReadReceiptGroup.tsx
index 716d24fa33..3472ee8db3 100644
--- a/src/components/views/rooms/ReadReceiptGroup.tsx
+++ b/src/components/views/rooms/ReadReceiptGroup.tsx
@@ -42,9 +42,9 @@ export const READ_AVATAR_SIZE = 16;
 interface Props {
     readReceipts: IReadReceiptProps[];
     readReceiptMap: { [userId: string]: IReadReceiptInfo };
-    checkUnmounting: () => boolean;
+    checkUnmounting?: () => boolean;
     suppressAnimation: boolean;
-    isTwelveHour: boolean;
+    isTwelveHour?: boolean;
 }
 
 interface IAvatarPosition {
@@ -169,8 +169,8 @@ export function ReadReceiptGroup({
         );
     }
 
-    let contextMenu;
-    if (menuDisplayed) {
+    let contextMenu: JSX.Element | undefined;
+    if (menuDisplayed && button.current) {
         const buttonRect = button.current.getBoundingClientRect();
         contextMenu = (
             <ContextMenu menuClassName="mx_ReadReceiptGroup_popup" onFinished={closeMenu} {...aboveLeftOf(buttonRect)}>
@@ -226,7 +226,7 @@ export function ReadReceiptGroup({
 }
 
 interface ReadReceiptPersonProps extends IReadReceiptProps {
-    isTwelveHour: boolean;
+    isTwelveHour?: boolean;
     onAfterClick?: () => void;
 }
 
diff --git a/src/components/views/rooms/ReadReceiptMarker.tsx b/src/components/views/rooms/ReadReceiptMarker.tsx
index 1a47719f58..e5e2fafdd7 100644
--- a/src/components/views/rooms/ReadReceiptMarker.tsx
+++ b/src/components/views/rooms/ReadReceiptMarker.tsx
@@ -48,7 +48,7 @@ interface IProps {
     suppressAnimation?: boolean;
 
     // an opaque object for storing information about this user's RR in this room
-    readReceiptInfo: IReadReceiptInfo;
+    readReceiptInfo?: IReadReceiptInfo;
 
     // A function which is used to check if the parent panel is being
     // unmounted, to avoid unnecessary work. Should return true if we
diff --git a/src/components/views/rooms/RoomPreviewBar.tsx b/src/components/views/rooms/RoomPreviewBar.tsx
index a968bd3794..c3f75c1f23 100644
--- a/src/components/views/rooms/RoomPreviewBar.tsx
+++ b/src/components/views/rooms/RoomPreviewBar.tsx
@@ -215,9 +215,9 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
         if (!myMember) {
             return {};
         }
-        const kickerMember = this.props.room.currentState.getMember(myMember.events.member.getSender());
+        const kickerMember = this.props.room?.currentState.getMember(myMember.events.member.getSender());
         const memberName = kickerMember ? kickerMember.name : myMember.events.member.getSender();
-        const reason = myMember.events.member.getContent().reason;
+        const reason = myMember.events.member?.getContent().reason;
         return { memberName, reason };
     }
 
@@ -252,8 +252,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
         if (!myMember) {
             return false;
         }
-        const memberEvent = myMember.events.member;
-        const memberContent = memberEvent.getContent();
+        const memberContent = myMember.events.member.getContent();
         return memberContent.membership === "invite" && memberContent.is_direct;
     }
 
@@ -397,7 +396,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
                 const errCodeMessage = _t(
                     "An error (%(errcode)s) was returned while trying to validate your " +
                         "invite. You could try to pass this information on to the person who invited you.",
-                    { errcode: this.state.threePidFetchError.errcode || _t("unknown error code") },
+                    { errcode: this.state.threePidFetchError?.errcode || _t("unknown error code") },
                 );
                 switch (joinRule) {
                     case "invite":
diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx
index 86e1f28ff1..87166c94a1 100644
--- a/src/components/views/rooms/RoomTile.tsx
+++ b/src/components/views/rooms/RoomTile.tsx
@@ -204,7 +204,7 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
 
     private async generatePreview(): Promise<void> {
         if (!this.showMessagePreview) {
-            return null;
+            return;
         }
 
         const messagePreview = await MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag);
diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts
index d03f45bcc5..30c8a3c491 100644
--- a/src/settings/SettingsStore.ts
+++ b/src/settings/SettingsStore.ts
@@ -511,7 +511,7 @@ export default class SettingsStore {
      * check at.
      * @return {boolean} True if the user may set the setting, false otherwise.
      */
-    public static canSetValue(settingName: string, roomId: string, level: SettingLevel): boolean {
+    public static canSetValue(settingName: string, roomId: string | null, level: SettingLevel): boolean {
         // Verify that the setting is actually a setting
         if (!SETTINGS[settingName]) {
             throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
diff --git a/src/utils/EditorStateTransfer.ts b/src/utils/EditorStateTransfer.ts
index a61e328d8c..1604d22530 100644
--- a/src/utils/EditorStateTransfer.ts
+++ b/src/utils/EditorStateTransfer.ts
@@ -30,7 +30,7 @@ export default class EditorStateTransfer {
 
     public constructor(private readonly event: MatrixEvent) {}
 
-    public setEditorState(caret: DocumentOffset, serializedParts: SerializedPart[]): void {
+    public setEditorState(caret: DocumentOffset | null, serializedParts: SerializedPart[]): void {
         this.caret = caret;
         this.serializedParts = serializedParts;
     }
diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts
index c33a90cc86..6f1b1a64fa 100644
--- a/src/utils/EventUtils.ts
+++ b/src/utils/EventUtils.ts
@@ -263,7 +263,7 @@ export function editEvent(
     }
 }
 
-export function canCancel(status: EventStatus): boolean {
+export function canCancel(status?: EventStatus | null): boolean {
     return status === EventStatus.QUEUED || status === EventStatus.NOT_SENT || status === EventStatus.ENCRYPTING;
 }
 
diff --git a/test/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx b/test/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx
index 4f7441f59a..6b57422974 100644
--- a/test/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx
+++ b/test/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx
@@ -80,7 +80,7 @@ describe("PreferencesUserSettingsTab", () => {
 
                 await waitFor(() => expect(toggle).toHaveAttribute("aria-disabled", "false"));
                 fireEvent.click(toggle);
-                expectSetValueToHaveBeenCalled("sendReadReceipts", undefined, SettingLevel.ACCOUNT, true);
+                expectSetValueToHaveBeenCalled("sendReadReceipts", null, SettingLevel.ACCOUNT, true);
             });
 
             it("can be disabled", async () => {
@@ -89,7 +89,7 @@ describe("PreferencesUserSettingsTab", () => {
 
                 await waitFor(() => expect(toggle).toHaveAttribute("aria-disabled", "false"));
                 fireEvent.click(toggle);
-                expectSetValueToHaveBeenCalled("sendReadReceipts", undefined, SettingLevel.ACCOUNT, false);
+                expectSetValueToHaveBeenCalled("sendReadReceipts", null, SettingLevel.ACCOUNT, false);
             });
         });
 
@@ -104,7 +104,7 @@ describe("PreferencesUserSettingsTab", () => {
 
                 await waitFor(() => expect(toggle).toHaveAttribute("aria-disabled", "false"));
                 fireEvent.click(toggle);
-                expectSetValueToHaveBeenCalled("sendReadReceipts", undefined, SettingLevel.ACCOUNT, true);
+                expectSetValueToHaveBeenCalled("sendReadReceipts", null, SettingLevel.ACCOUNT, true);
             });
 
             it("cannot be disabled", async () => {