From a4051330a36933ad5f5e1a442658a4bafc231aa9 Mon Sep 17 00:00:00 2001
From: bree <me@bree.dev>
Date: Sun, 4 Jul 2021 07:09:39 -0400
Subject: [PATCH] remove most uses of `as any` in typescript

- replaced many uses of `as any` with another more specific cast `as T`
- filled in missing typed for items that needed to be typed
  - new runtime code was added where necessary to satisfy the new types with comments
- added missing theme variable "sidebar-active" to the Theme variables
- forms using `react-hook-form` are now typechecked
- changed some instances of `target` into `currentTarget` while removing `as any` assertions
---
 src/components/common/AutoComplete.tsx        |  2 +-
 src/components/common/LocaleSelector.tsx      |  6 ++---
 src/components/common/UpdateIndicator.tsx     |  2 +-
 src/components/common/messaging/Message.tsx   |  7 ++++-
 .../attachments/AttachmentActions.tsx         |  3 ++-
 src/components/markdown/Renderer.tsx          | 27 ++++++++++++++-----
 .../navigation/left/HomeSidebar.tsx           |  7 ++---
 .../navigation/right/MemberSidebar.tsx        | 16 +++--------
 src/components/ui/Category.tsx                |  1 +
 src/context/Locale.tsx                        |  6 +++--
 src/context/Settings.tsx                      |  4 +--
 src/context/Theme.tsx                         | 12 ++++++---
 src/context/Voice.tsx                         |  6 +++--
 src/context/intermediate/modals/Input.tsx     |  3 ++-
 .../intermediate/modals/Onboarding.tsx        | 12 ++++++---
 .../intermediate/popovers/ModifyAccount.tsx   | 27 ++++++++++++-------
 src/context/revoltjs/FileUploads.tsx          |  4 +--
 src/context/revoltjs/RevoltClient.tsx         | 15 +++++------
 src/context/revoltjs/SyncManager.tsx          |  4 +--
 src/context/revoltjs/events.ts                | 27 +++++++++----------
 src/context/revoltjs/hooks.ts                 | 14 ++++++++--
 src/lib/ContextMenus.tsx                      | 20 +++++++-------
 src/pages/login/FormField.tsx                 |  2 ++
 src/pages/login/forms/Form.tsx                | 17 +++++++-----
 src/pages/login/forms/FormReset.tsx           |  4 +--
 src/pages/settings/panes/Account.tsx          |  6 ++---
 src/pages/settings/panes/Appearance.tsx       | 10 +++----
 src/pages/settings/panes/Notifications.tsx    |  6 ++---
 src/pages/settings/panes/Profile.tsx          |  4 +--
 src/pages/settings/panes/Sessions.tsx         |  2 +-
 src/pages/settings/server/Roles.tsx           |  2 +-
 31 files changed, 161 insertions(+), 117 deletions(-)

diff --git a/src/components/common/AutoComplete.tsx b/src/components/common/AutoComplete.tsx
index 0e0f399..565f4a4 100644
--- a/src/components/common/AutoComplete.tsx
+++ b/src/components/common/AutoComplete.tsx
@@ -384,7 +384,7 @@ export default function AutoComplete({ detached, state, setState, onClick }: Pic
                                 })
                             }
                             onClick={onClick}>
-                            <Emoji emoji={(emojiDictionary as any)[match]} size={20} />
+                            <Emoji emoji={(emojiDictionary as Record<string, string>)[match]} size={20} />
                             :{match}:
                         </button>
                     ))}
diff --git a/src/components/common/LocaleSelector.tsx b/src/components/common/LocaleSelector.tsx
index 9e68cd7..225e2c3 100644
--- a/src/components/common/LocaleSelector.tsx
+++ b/src/components/common/LocaleSelector.tsx
@@ -1,7 +1,7 @@
 import ComboBox from "../ui/ComboBox";
 import { connectState } from "../../redux/connector";
 import { WithDispatcher } from "../../redux/reducers";
-import { LanguageEntry, Languages } from "../../context/Locale";
+import { Language, LanguageEntry, Languages } from "../../context/Locale";
 
 type Props = WithDispatcher & {
     locale: string;
@@ -15,12 +15,12 @@ export function LocaleSelector(props: Props) {
                 props.dispatcher &&
                 props.dispatcher({
                     type: "SET_LOCALE",
-                    locale: e.currentTarget.value as any
+                    locale: e.currentTarget.value as Language
                 })
             }
         >
             {Object.keys(Languages).map(x => {
-                const l = (Languages as any)[x] as LanguageEntry;
+                const l = Languages[x as keyof typeof Languages];
                 return (
                     <option value={x}>
                         {l.emoji} {l.display}
diff --git a/src/components/common/UpdateIndicator.tsx b/src/components/common/UpdateIndicator.tsx
index d6730eb..111cd47 100644
--- a/src/components/common/UpdateIndicator.tsx
+++ b/src/components/common/UpdateIndicator.tsx
@@ -15,7 +15,7 @@ export default function UpdateIndicator() {
         return internalSubscribe('PWA', 'update', () => setPending(true));
     });
 
-    if (!pending) return;
+    if (!pending) return <></>;
     const theme = useContext(ThemeContext);
 
     return (
diff --git a/src/components/common/messaging/Message.tsx b/src/components/common/messaging/Message.tsx
index 46edea7..f00aad6 100644
--- a/src/components/common/messaging/Message.tsx
+++ b/src/components/common/messaging/Message.tsx
@@ -35,7 +35,12 @@ function Message({ attachContext, message, contrast, content: replacement, head:
     const content = message.content as string;
     const head = preferHead || (message.replies && message.replies.length > 0);
 
-    const userContext = attachContext ? attachContextMenu('Menu', { user: message.author, contextualChannel: message.channel }) : undefined as any; // ! FIXME: tell fatal to make this type generic
+     // ! FIXME: tell fatal to make this type generic
+    // bree: Fatal please...
+    const userContext = attachContext 
+        ? attachContextMenu('Menu', { user: message.author, contextualChannel: message.channel }) as any
+        : undefined;
+
     const openProfile = () => openScreen({ id: 'profile', user_id: message.author });
 
     return (
diff --git a/src/components/common/messaging/attachments/AttachmentActions.tsx b/src/components/common/messaging/attachments/AttachmentActions.tsx
index f667f16..28e3157 100644
--- a/src/components/common/messaging/attachments/AttachmentActions.tsx
+++ b/src/components/common/messaging/attachments/AttachmentActions.tsx
@@ -18,7 +18,8 @@ export default function AttachmentActions({ attachment }: Props) {
     const open_url = `${url}/${filename}`;
     const download_url = url.replace('attachments', 'attachments/download')
 
-    const filesize = determineFileSize(size as any);
+    // for some reason revolt.js says the size is a string even though it's a number
+    const filesize = determineFileSize(size as unknown as number);
 
     switch (metadata.type) {
         case 'Image':
diff --git a/src/components/markdown/Renderer.tsx b/src/components/markdown/Renderer.tsx
index 115cad6..61575a8 100644
--- a/src/components/markdown/Renderer.tsx
+++ b/src/components/markdown/Renderer.tsx
@@ -22,13 +22,21 @@ import MarkdownSup from "markdown-it-sup";
 // @ts-ignore
 import MarkdownSub from "markdown-it-sub";
 
+// TODO: global.d.ts file for defining globals
+declare global {
+    interface Window {
+        copycode: (element: HTMLDivElement) => void
+    }
+}
+
+
 // Handler for code block copy.
 if (typeof window !== "undefined") {
-    (window as any).copycode = function(element: HTMLDivElement) {
+    window.copycode = function(element: HTMLDivElement) {
         try {
             let code = element.parentElement?.parentElement?.children[1];
             if (code) {
-                navigator.clipboard.writeText((code as any).innerText.trim());
+                navigator.clipboard.writeText(code.textContent?.trim() ?? '');
             }
         } catch (e) {}
     };
@@ -65,10 +73,17 @@ const defaultRender =
         return self.renderToken(tokens, idx, options);
     };
 
+// TODO: global.d.ts file for defining globals
+declare global {
+    interface Window {
+        internalHandleURL: (element: HTMLAnchorElement) => void
+    }
+}
+
 // Handler for internal links, pushes events to React using magic.
 if (typeof window !== "undefined") {
-    (window as any).internalHandleURL = function(element: HTMLAnchorElement) {
-        const url = new URL(element.href, location as any);
+    window.internalHandleURL = function(element: HTMLAnchorElement) {
+        const url = new URL(element.href, location.href);
         const pathname = url.pathname;
 
         if (pathname.startsWith("/@")) {
@@ -87,7 +102,7 @@ md.renderer.rules.link_open = function(tokens, idx, options, env, self) {
             // For internal links, we should use our own handler to use react-router history.
             // @ts-ignore
             const href = tokens[idx].attrs[hIndex][1];
-            const url = new URL(href, location as any);
+            const url = new URL(href, location.href);
 
             if (url.hostname === location.hostname) {
                 internal = true;
@@ -161,7 +176,7 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
             data-large-emojis={useLargeEmojis}
             onClick={ev => {
                 if (ev.target) {
-                    let element: Element = ev.target as any;
+                    let element = ev.currentTarget;
                     if (element.classList.contains("spoiler")) {
                         element.classList.add("shown");
                     }
diff --git a/src/components/navigation/left/HomeSidebar.tsx b/src/components/navigation/left/HomeSidebar.tsx
index edde0aa..cd98bc7 100644
--- a/src/components/navigation/left/HomeSidebar.tsx
+++ b/src/components/navigation/left/HomeSidebar.tsx
@@ -107,11 +107,8 @@ function HomeSidebar(props: Props) {
                 )}
                 <Localizer>
                     <Category
-                        text={
-                            (
-                                <Text id="app.main.categories.conversations" />
-                            ) as any
-                        }
+                        text={<Text id="app.main.categories.conversations" />}
+                        /** @ts-ignore : ignored due to conflicting naming between the Category property name and the existing JSX attribute */
                         action={() => openScreen({ id: "special_input", type: "create_group" })}
                     />
                 </Localizer>
diff --git a/src/components/navigation/right/MemberSidebar.tsx b/src/components/navigation/right/MemberSidebar.tsx
index 96b3728..c5bcaf6 100644
--- a/src/components/navigation/right/MemberSidebar.tsx
+++ b/src/components/navigation/right/MemberSidebar.tsx
@@ -55,12 +55,8 @@ export function GroupMemberSidebar({ channel, ctx }: Props & { channel: Channels
 
     members.sort((a, b) => {
         // ! FIXME: should probably rewrite all this code
-        let l = ((a.online &&
-            a.status?.presence !== Users.Presence.Invisible) ??
-            false) as any | 0;
-        let r = ((b.online &&
-            b.status?.presence !== Users.Presence.Invisible) ??
-            false) as any | 0;
+        let l = +((a.online && a.status?.presence !== Users.Presence.Invisible) ?? false) | 0;
+        let r = +((b.online && b.status?.presence !== Users.Presence.Invisible) ?? false) | 0;
 
         let n = r - l;
         if (n !== 0) {
@@ -159,12 +155,8 @@ export function ServerMemberSidebar({ channel, ctx }: Props & { channel: Channel
     // copy paste from above
     users.sort((a, b) => {
         // ! FIXME: should probably rewrite all this code
-        let l = ((a.online &&
-            a.status?.presence !== Users.Presence.Invisible) ??
-            false) as any | 0;
-        let r = ((b.online &&
-            b.status?.presence !== Users.Presence.Invisible) ??
-            false) as any | 0;
+        let l = +((a.online && a.status?.presence !== Users.Presence.Invisible) ?? false) | 0;
+        let r = +((b.online && b.status?.presence !== Users.Presence.Invisible) ?? false) | 0;
 
         let n = r - l;
         if (n !== 0) {
diff --git a/src/components/ui/Category.tsx b/src/components/ui/Category.tsx
index ccdd4e9..79f1d0b 100644
--- a/src/components/ui/Category.tsx
+++ b/src/components/ui/Category.tsx
@@ -33,6 +33,7 @@ const CategoryBase = styled.div<Pick<Props, 'variant'>>`
 
 type Props = Omit<JSX.HTMLAttributes<HTMLDivElement>, 'children' | 'as'> & {
     text: Children;
+    // TODO: rename from action to prevent type conflicts with the dom
     action?: () => void;
     variant?: 'default' | 'uniform';
 }
diff --git a/src/context/Locale.tsx b/src/context/Locale.tsx
index 0385745..af894b4 100644
--- a/src/context/Locale.tsx
+++ b/src/context/Locale.tsx
@@ -122,9 +122,11 @@ interface Props {
 }
 
 function Locale({ children, locale }: Props) {
-    const [defns, setDefinition] = useState(definition);
+    // TODO: create and use LanguageDefinition type here
+    const [defns, setDefinition] = useState<Record<string, unknown>>(definition);
     const lang = Languages[locale];
 
+    // TOOD: clean this up and use the built in Intl API
     function transformLanguage(obj: { [key: string]: any }) {
         const dayjs = obj.dayjs;
         const defaults = dayjs.defaults;
@@ -158,7 +160,7 @@ function Locale({ children, locale }: Props) {
 
         if (lang.i18n === "hardcore") {
             // eslint-disable-next-line @typescript-eslint/no-explicit-any
-            setDefinition({} as any);
+            setDefinition({});
             return;
         }
 
diff --git a/src/context/Settings.tsx b/src/context/Settings.tsx
index 7f834fe..3653b93 100644
--- a/src/context/Settings.tsx
+++ b/src/context/Settings.tsx
@@ -15,8 +15,8 @@ import { Children } from "../types/Preact";
 import { createContext } from "preact";
 import { useMemo } from "preact/hooks";
 
-export const SettingsContext = createContext<Settings>({} as any);
-export const SoundContext = createContext<(sound: Sounds) => void>({} as any);
+export const SettingsContext = createContext<Settings>({});
+export const SoundContext = createContext<((sound: Sounds) => void)>(null!);
 
 interface Props {
     children?: Children,
diff --git a/src/context/Theme.tsx b/src/context/Theme.tsx
index f0e6db4..b6131c6 100644
--- a/src/context/Theme.tsx
+++ b/src/context/Theme.tsx
@@ -30,7 +30,8 @@ export type Variables =
     | "status-away"
     | "status-busy"
     | "status-streaming"
-    | "status-invisible";
+    | "status-invisible"
+    | "sidebar-active";
 
 export type Theme = {
     [variable in Variables]: string;
@@ -45,7 +46,7 @@ export interface ThemeOptions {
 }
 
 // Generated from https://gitlab.insrt.uk/revolt/community/themes
-export const PRESETS: { [key: string]: Theme } = {
+export const PRESETS: Record<string, Theme> = {
     light: {
         light: true,
         accent: "#FD6671",
@@ -72,6 +73,7 @@ export const PRESETS: { [key: string]: Theme } = {
         "status-busy": "#F84848",
         "status-streaming": "#977EFF",
         "status-invisible": "#A5A5A5",
+        "sidebar-active": "var(--secondary-background)"
     },
     dark: {
         light: false,
@@ -99,6 +101,7 @@ export const PRESETS: { [key: string]: Theme } = {
         "status-busy": "#F84848",
         "status-streaming": "#977EFF",
         "status-invisible": "#A5A5A5",
+        "sidebar-active": "var(--secondary-background)"
     },
 };
 
@@ -113,7 +116,8 @@ const GlobalTheme = createGlobalStyle<{ theme: Theme }>`
 }
 `;
 
-export const ThemeContext = createContext<Theme>({} as any);
+// Load the default default them and apply extras later
+export const ThemeContext = createContext<Theme>(PRESETS['dark']);
 
 interface Props {
     children: Children;
@@ -123,7 +127,7 @@ interface Props {
 function Theme(props: Props) {
     const theme: Theme = {
         ...PRESETS["dark"],
-        ...(PRESETS as any)[props.options?.preset as any],
+        ...PRESETS[props.options?.preset ?? ''],
         ...props.options?.custom
     };
 
diff --git a/src/context/Voice.tsx b/src/context/Voice.tsx
index ac5c407..2f6fedf 100644
--- a/src/context/Voice.tsx
+++ b/src/context/Voice.tsx
@@ -33,8 +33,10 @@ export interface VoiceState {
     participants?: Readonly<Map<string, VoiceUser>>;
 }
 
-export const VoiceContext = createContext<VoiceState>(undefined as any);
-export const VoiceOperationsContext = createContext<VoiceOperations>(undefined as any);
+// [bree] TODO: I feel like these should be typechecked anyways but whatever,
+// I'm asserting that they aren't null because they get used near immedietly from what I can tell
+export const VoiceContext = createContext<VoiceState>(null!);
+export const VoiceOperationsContext = createContext<VoiceOperations>(null!);
 
 type Props = {
     children: Children;
diff --git a/src/context/intermediate/modals/Input.tsx b/src/context/intermediate/modals/Input.tsx
index d57ba8d..ec4c9de 100644
--- a/src/context/intermediate/modals/Input.tsx
+++ b/src/context/intermediate/modals/Input.tsx
@@ -119,8 +119,9 @@ export function SpecialInputModal(props: SpecialProps) {
                 question={<Text id="app.settings.permissions.create_role" />}
                 field={<Text id="app.settings.permissions.role_name" />}
                 callback={async name => {
+                    // bree: this returns void, dunno why props.callback was being called
                     const role = await client.servers.createRole(props.server, name);
-                    props.callback(role.id);
+                    // props.callback(role.id);
                 }}
             />;
         }
diff --git a/src/context/intermediate/modals/Onboarding.tsx b/src/context/intermediate/modals/Onboarding.tsx
index 8f3894d..3f32819 100644
--- a/src/context/intermediate/modals/Onboarding.tsx
+++ b/src/context/intermediate/modals/Onboarding.tsx
@@ -1,6 +1,6 @@
 import { Text } from "preact-i18n";
 import { useState } from "preact/hooks";
-import { useForm } from "react-hook-form";
+import { SubmitHandler, useForm } from "react-hook-form";
 import styles from "./Onboarding.module.scss";
 import { takeError } from "../../revoltjs/util";
 import Button from "../../../components/ui/Button";
@@ -14,12 +14,16 @@ interface Props {
     callback: (username: string, loginAfterSuccess?: true) => Promise<void>;
 }
 
+interface FormInputs {
+    username: string
+}
+
 export function OnboardingModal({ onClose, callback }: Props) {
-    const { handleSubmit, register } = useForm();
+    const { handleSubmit, register } = useForm<FormInputs>();
     const [loading, setLoading] = useState(false);
     const [error, setError] = useState<string | undefined>(undefined);
 
-    async function onSubmit({ username }: { username: string }) {
+    const onSubmit: SubmitHandler<FormInputs> = ({ username }) => {
         setLoading(true);
         callback(username, true)
             .then(onClose)
@@ -45,7 +49,7 @@ export function OnboardingModal({ onClose, callback }: Props) {
                         <p>
                             <Text id="app.special.modals.onboarding.pick" />
                         </p>
-                        <form onSubmit={handleSubmit(onSubmit) as any}>
+                        <form onSubmit={handleSubmit(onSubmit) as JSX.GenericEventHandler<HTMLFormElement>}>
                             <div>
                                 <FormField
                                     type="username"
diff --git a/src/context/intermediate/popovers/ModifyAccount.tsx b/src/context/intermediate/popovers/ModifyAccount.tsx
index 2deb2b8..25477c0 100644
--- a/src/context/intermediate/popovers/ModifyAccount.tsx
+++ b/src/context/intermediate/popovers/ModifyAccount.tsx
@@ -1,5 +1,5 @@
 import { Text } from "preact-i18n";
-import { useForm } from "react-hook-form";
+import { SubmitHandler, useForm } from "react-hook-form";
 import Modal from "../../../components/ui/Modal";
 import { takeError } from "../../revoltjs/util";
 import { useContext, useState } from "preact/hooks";
@@ -12,22 +12,28 @@ interface Props {
     field: "username" | "email" | "password";
 }
 
+interface FormInputs {
+    password: string,
+    new_email: string,
+    new_username: string,
+    new_password: string,
+
+    // TODO: figure out if this is correct or not
+    // it wasn't in the types before this was typed but the element itself was there
+    current_password?: string
+}
+
 export function ModifyAccountModal({ onClose, field }: Props) {
     const client = useContext(AppContext);
-    const { handleSubmit, register, errors } = useForm();
+    const { handleSubmit, register, errors } = useForm<FormInputs>();
     const [error, setError] = useState<string | undefined>(undefined);
 
-    async function onSubmit({
+    const onSubmit: SubmitHandler<FormInputs> = async ({
         password,
         new_username,
         new_email,
         new_password
-    }: {
-        password: string;
-        new_username: string;
-        new_email: string;
-        new_password: string;
-    }) {
+    }) => {
         try {
             if (field === "email") {
                 await client.req("POST", "/auth/change/email", {
@@ -75,7 +81,8 @@ export function ModifyAccountModal({ onClose, field }: Props) {
                 }
             ]}
         >
-            <form onSubmit={handleSubmit(onSubmit) as any}>
+            {/* Preact / React typing incompatabilities */}
+            <form onSubmit={handleSubmit(onSubmit) as JSX.GenericEventHandler<HTMLFormElement>}>
                 {field === "email" && (
                     <FormField
                         type="email"
diff --git a/src/context/revoltjs/FileUploads.tsx b/src/context/revoltjs/FileUploads.tsx
index ab9156b..95f50e8 100644
--- a/src/context/revoltjs/FileUploads.tsx
+++ b/src/context/revoltjs/FileUploads.tsx
@@ -43,8 +43,8 @@ export function grabFiles(maxFileSize: number, cb: (files: File[]) => void, tooL
     input.type = "file";
     input.multiple = multiple ?? false;
 
-    input.onchange = async e => {
-        const files = (e.target as any)?.files;
+    input.onchange = async (e) => {
+        const files = (e.currentTarget as HTMLInputElement)?.files;
         if (!files) return;
         for (let file of files) {
             if (file.size > maxFileSize) {
diff --git a/src/context/revoltjs/RevoltClient.tsx b/src/context/revoltjs/RevoltClient.tsx
index 0861782..99ad0eb 100644
--- a/src/context/revoltjs/RevoltClient.tsx
+++ b/src/context/revoltjs/RevoltClient.tsx
@@ -34,9 +34,10 @@ export interface ClientOperations {
     openDM: (user_id: string) => Promise<string>;
 }
 
-export const AppContext = createContext<Client>(undefined as any);
-export const StatusContext = createContext<ClientStatus>(undefined as any);
-export const OperationsContext = createContext<ClientOperations>(undefined as any);
+// TODO: remove temporary non-null assertions and properly typecheck these as they aren't always immedietely initialized
+export const AppContext = createContext<Client>(null!);
+export const StatusContext = createContext<ClientStatus>(null!);
+export const OperationsContext = createContext<ClientOperations>(null!);
 
 type Props = WithDispatcher & {
     auth: AuthState;
@@ -93,16 +94,14 @@ function Context({ auth, children, dispatcher }: Props) {
                     const login = () =>
                         dispatcher({
                             type: "LOGIN",
-                            session: client.session as any
+                            session: client.session! // TODO: verify that this null assertion is correct
                         });
 
                     if (onboarding) {
                         openScreen({
                             id: "onboarding",
-                            callback: async (username: string) => {
-                                await (onboarding as any)(username, true);
-                                login();
-                            }
+                            callback: (username: string) =>
+                                onboarding(username, true).then(login)
                         });
                     } else {
                         login();
diff --git a/src/context/revoltjs/SyncManager.tsx b/src/context/revoltjs/SyncManager.tsx
index d3fd7ca..99842b4 100644
--- a/src/context/revoltjs/SyncManager.tsx
+++ b/src/context/revoltjs/SyncManager.tsx
@@ -23,11 +23,11 @@ type Props = WithDispatcher & {
 
 var lastValues: { [key in SyncKeys]?: any } = { };
 
-export function mapSync(packet: Sync.UserSettings, revision?: { [key: string]: number }) {
+export function mapSync(packet: Sync.UserSettings, revision?: Record<string, number>) {
     let update: { [key in SyncKeys]?: [ number, SyncData[key] ] } = {};
     for (let key of Object.keys(packet)) {
         let [ timestamp, obj ] = packet[key];
-        if (timestamp < (revision ?? {} as any)[key] ?? 0) {
+        if (timestamp < (revision ?? {})[key] ?? 0) {
             continue;
         }
 
diff --git a/src/context/revoltjs/events.ts b/src/context/revoltjs/events.ts
index 806ddce..11732da 100644
--- a/src/context/revoltjs/events.ts
+++ b/src/context/revoltjs/events.ts
@@ -32,7 +32,7 @@ export function registerEvents({
         }
     }
 
-    const listeners = {
+    let listeners: Record<string, (...args: any[]) => void> = {
         connecting: () =>
             operations.ready() && setStatus(ClientStatus.CONNECTING),
 
@@ -87,21 +87,18 @@ export function registerEvents({
         ready: () => setStatus(ClientStatus.ONLINE)
     };
 
-    let listenerFunc: { [key: string]: Function };
     if (import.meta.env.DEV) {
-        listenerFunc = {};
-        for (const listener of Object.keys(listeners)) {
-            listenerFunc[listener] = (...args: any[]) => {
-                console.debug(`Calling ${listener} with`, args);
-                (listeners as any)[listener](...args);
-            };
-        }
-    } else {
-        listenerFunc = listeners;
+        listeners = new Proxy(listeners, {
+            get: (target, listener, receiver) => (...args: unknown[]) => {
+                console.debug(`Calling ${listener.toString()} with`, args);
+                Reflect.get(target, listener)(...args)
+            }
+        })
     }
 
-    for (const listener of Object.keys(listenerFunc)) {
-        client.addListener(listener, (listenerFunc as any)[listener]);
+    // TODO: clean this a bit and properly handle types
+    for (const listener in listeners) {
+        client.addListener(listener, listeners[listener]);
     }
 
     function logMutation(target: string, key: string) {
@@ -135,8 +132,8 @@ export function registerEvents({
     window.addEventListener("offline", offline);
 
     return () => {
-        for (const listener of Object.keys(listenerFunc)) {
-            client.removeListener(listener, (listenerFunc as any)[listener]);
+        for (const listener in listeners) {
+            client.removeListener(listener, listeners[listener as keyof typeof listeners]);
         }
 
         if (import.meta.env.DEV) {
diff --git a/src/context/revoltjs/hooks.ts b/src/context/revoltjs/hooks.ts
index f57038b..fac8898 100644
--- a/src/context/revoltjs/hooks.ts
+++ b/src/context/revoltjs/hooks.ts
@@ -2,6 +2,7 @@ import { useCallback, useContext, useEffect, useState } from "preact/hooks";
 import { Channels, Servers, Users } from "revolt.js/dist/api/objects";
 import { Client, PermissionCalculator } from 'revolt.js';
 import { AppContext } from "./RevoltClient";
+import Collection from "revolt.js/dist/maps/Collection";
 
 export interface HookContext {
     client: Client,
@@ -25,7 +26,16 @@ export function useForceUpdate(context?: HookContext): HookContext {
     return { client, forceUpdate: () => updateState(Math.random()) };
 }
 
-function useObject(type: string, id?: string | string[], context?: HookContext) {
+// TODO: utils.d.ts maybe?
+type PickProperties<T, U> = Pick<T, {
+    [K in keyof T]: T[K] extends U ? K : never
+}[keyof T]>
+
+// The keys in Client that are an object
+// for some reason undefined keeps appearing despite there being no reason to so it's filtered out
+type ClientCollectionKey = Exclude<keyof PickProperties<Client, Collection<any>>, undefined>;
+
+function useObject(type: ClientCollectionKey, id?: string | string[], context?: HookContext) {
     const ctx = useForceUpdate(context);
 
     function update(target: any) {
@@ -35,7 +45,7 @@ function useObject(type: string, id?: string | string[], context?: HookContext)
         }
     }
 
-    const map = (ctx.client as any)[type];
+    const map = ctx.client[type];
     useEffect(() => {
         map.addListener("update", update);
         return () => map.removeListener("update", update);
diff --git a/src/lib/ContextMenus.tsx b/src/lib/ContextMenus.tsx
index 7252d8b..4b7a868 100644
--- a/src/lib/ContextMenus.tsx
+++ b/src/lib/ContextMenus.tsx
@@ -307,11 +307,15 @@ function ContextMenus(props: Props) {
                 case "delete_server":
                 case "delete_message":
                 case "create_channel":
-                // @ts-expect-error
-                case "create_invite": openScreen({ id: "special_prompt", type: data.action, target: data.target }); break;
+                case "create_invite":
+                    // The any here is because typescript flattens the case types into a single type and type structure and specifity is lost or whatever
+                    openScreen({ id: "special_prompt", type: data.action, target: data.target as any }); 
+                    break;
 
                 case "ban_member":
-                case "kick_member": openScreen({ id: "special_prompt", type: data.action, target: data.target, user: data.user }); break;
+                case "kick_member": 
+                    openScreen({ id: "special_prompt", type: data.action, target: data.target, user: data.user });
+                    break;
 
                 case "open_notification_options": {
                     openContextMenu("NotificationOptions", { channel: data.channel });
@@ -427,7 +431,7 @@ function ContextMenus(props: Props) {
                     }
 
                     if (user) {
-                        let actions: string[];
+                        let actions: Action['action'][];
                         switch (user.relationship) {
                             case Users.Relationship.User: actions = []; break;
                             case Users.Relationship.Friend:
@@ -461,11 +465,9 @@ function ContextMenus(props: Props) {
                             generateAction({ action: 'message_user', user: user._id });
                         }
 
-                        for (const action of actions) {
-                            generateAction({
-                                action: action as any,
-                                user
-                            });
+                        for(let i = 0; i < actions.length; i++) {
+                            // The any here is because typescript can't determine that user the actions are linked together correctly
+                            generateAction({ action: actions[i] as any, user })
                         }
                     }
 
diff --git a/src/pages/login/FormField.tsx b/src/pages/login/FormField.tsx
index eb7f498..94a3609 100644
--- a/src/pages/login/FormField.tsx
+++ b/src/pages/login/FormField.tsx
@@ -26,6 +26,8 @@ export default function FormField({
             )}
             <Localizer>
                 <InputBox
+                    // Styled uses React typing while we use Preact
+                    // this leads to inconsistances where things need to be typed oddly
                     placeholder={(<Text id={`login.enter.${type}`} />) as any}
                     name={
                         type === "current_password" ? "password" : name ?? type
diff --git a/src/pages/login/forms/Form.tsx b/src/pages/login/forms/Form.tsx
index d1e0e8c..66791ec 100644
--- a/src/pages/login/forms/Form.tsx
+++ b/src/pages/login/forms/Form.tsx
@@ -35,6 +35,12 @@ function getInviteCode() {
     return code ?? '';
 }
 
+interface FormInputs {
+    email: string
+    password: string
+    invite: string
+}
+
 export function Form({ page, callback }: Props) {
     const client = useContext(AppContext);
 
@@ -43,7 +49,7 @@ export function Form({ page, callback }: Props) {
     const [error, setGlobalError] = useState<string | undefined>(undefined);
     const [captcha, setCaptcha] = useState<CaptchaProps | undefined>(undefined);
 
-    const { handleSubmit, register, errors, setError } = useForm({
+    const { handleSubmit, register, errors, setError } = useForm<FormInputs>({
         defaultValues: {
             email: '',
             password: '',
@@ -51,11 +57,7 @@ export function Form({ page, callback }: Props) {
         }
     });
 
-    async function onSubmit(data: {
-        email: string;
-        password: string;
-        invite: string;
-    }) {
+    async function onSubmit(data: FormInputs) {
         setGlobalError(undefined);
         setLoading(true);
 
@@ -143,7 +145,8 @@ export function Form({ page, callback }: Props) {
     return (
         <div className={styles.form}>
             <img src={wideSVG} />
-            <form onSubmit={handleSubmit(onSubmit) as any}>
+            {/* Preact / React typing incompatabilities */}
+            <form onSubmit={handleSubmit(onSubmit) as JSX.GenericEventHandler<HTMLFormElement>}>
                 {page !== "reset" && (
                     <FormField
                         type="email"
diff --git a/src/pages/login/forms/FormReset.tsx b/src/pages/login/forms/FormReset.tsx
index a8a7886..ac73d01 100644
--- a/src/pages/login/forms/FormReset.tsx
+++ b/src/pages/login/forms/FormReset.tsx
@@ -25,9 +25,9 @@ export function FormReset() {
         <Form
             page="reset"
             callback={async data => {
-                await client.req("POST", "/auth/reset" as any, {
+                await client.req("POST", "/auth/reset", {
                     token,
-                    ...(data as any)
+                    ...data
                 });
                 history.push("/login");
             }}
diff --git a/src/pages/settings/panes/Account.tsx b/src/pages/settings/panes/Account.tsx
index 1fc80c2..7fd1021 100644
--- a/src/pages/settings/panes/Account.tsx
+++ b/src/pages/settings/panes/Account.tsx
@@ -53,11 +53,11 @@ export function Account() {
                 <div className={styles.username}>@{user.username}</div>
             </div>
             <div className={styles.details}>
-                {[
+                {([
                     ["username", user.username, <At size={24} />],
                     ["email", email, <Envelope size={24} />],
                     ["password", "*****", <Key size={24} />]
-                ].map(([field, value, icon]) => (
+                ] as const).map(([field, value, icon]) => (
                     <div>
                         {icon}
                         <div className={styles.detail}>
@@ -71,7 +71,7 @@ export function Account() {
                                 onClick={() =>
                                     openScreen({
                                         id: "modify_account",
-                                        field: field as any
+                                        field: field
                                     })
                                 }
                                 contrast
diff --git a/src/pages/settings/panes/Appearance.tsx b/src/pages/settings/panes/Appearance.tsx
index b0ed1da..fdf209d 100644
--- a/src/pages/settings/panes/Appearance.tsx
+++ b/src/pages/settings/panes/Appearance.tsx
@@ -208,7 +208,7 @@ export function Component(props: Props & WithDispatcher) {
                     </Button>
                 </div>
                 <div className={styles.overrides}>
-                    {[
+                    {([
                         "accent",
                         "background",
                         "foreground",
@@ -234,15 +234,15 @@ export function Component(props: Props & WithDispatcher) {
                         "warning",
                         "error",
                         "hover"
-                    ].map(x => (
+                    ] as const).map(x => (
                         <div className={styles.entry} key={x}>
                             <span>{x}</span>
                             <div className={styles.override}>
                                 <div className={styles.picker}
-                                    style={{ backgroundColor: (theme as any)[x as any] }}>
+                                    style={{ backgroundColor: theme[x] }}>
                                     <input
                                         type="color"
-                                        value={(theme as any)[x as any]}
+                                        value={theme[x]}
                                         onChange={v =>
                                             setOverride({
                                                 [x]: v.currentTarget.value
@@ -252,7 +252,7 @@ export function Component(props: Props & WithDispatcher) {
                                 </div>
                                 <InputBox
                                     className={styles.text}
-                                    value={(theme as any)[x as any]}
+                                    value={theme[x]}
                                     onChange={y =>
                                         setOverride({
                                             [x]: y.currentTarget.value
diff --git a/src/pages/settings/panes/Notifications.tsx b/src/pages/settings/panes/Notifications.tsx
index 1dca0cd..6122d28 100644
--- a/src/pages/settings/panes/Notifications.tsx
+++ b/src/pages/settings/panes/Notifications.tsx
@@ -78,11 +78,11 @@ export function Component({ options, dispatcher }: Props & WithDispatcher) {
 
                                 // tell the server we just subscribed
                                 const json = sub.toJSON();
-                                if (json.keys) {
+                                if (json.keys) {;
                                     client.req("POST", "/push/subscribe", {
                                         endpoint: sub.endpoint,
-                                        ...json.keys
-                                    } as any);
+                                        ...(json.keys as { p256dh: string, auth: string })
+                                    });
                                     setPushEnabled(true);
                                 }
                             } else {
diff --git a/src/pages/settings/panes/Profile.tsx b/src/pages/settings/panes/Profile.tsx
index 941d311..1b6fa42 100644
--- a/src/pages/settings/panes/Profile.tsx
+++ b/src/pages/settings/panes/Profile.tsx
@@ -11,7 +11,7 @@ import { ClientStatus, StatusContext } from "../../../context/revoltjs/RevoltCli
 import AutoComplete, { useAutoComplete } from "../../../components/common/AutoComplete";
 
 export function Profile() {
-    const { intl } = useContext(IntlContext) as any;
+    const { intl } = useContext(IntlContext);
     const status = useContext(StatusContext);
 
     const ctx = useForceUpdate();
@@ -121,7 +121,7 @@ export function Profile() {
                             : "placeholder"
                     }`,
                     "",
-                    intl.dictionary
+                    (intl as any).dictionary as Record<string, unknown>
                 )}
                 onKeyUp={onKeyUp}
                 onKeyDown={onKeyDown}
diff --git a/src/pages/settings/panes/Sessions.tsx b/src/pages/settings/panes/Sessions.tsx
index b9bccc4..2ec357b 100644
--- a/src/pages/settings/panes/Sessions.tsx
+++ b/src/pages/settings/panes/Sessions.tsx
@@ -155,7 +155,7 @@ export function Sessions() {
                                     ]);
                                     await client.req(
                                         "DELETE",
-                                        `/auth/sessions/${session.id}` as any
+                                        `/auth/sessions/${session.id}` as '/auth/sessions'
                                     );
                                     setSessions(
                                         sessions?.filter(
diff --git a/src/pages/settings/server/Roles.tsx b/src/pages/settings/server/Roles.tsx
index db3887c..cd827fe 100644
--- a/src/pages/settings/server/Roles.tsx
+++ b/src/pages/settings/server/Roles.tsx
@@ -30,7 +30,7 @@ export function Roles({ server }: Props) {
 
     if (role !== 'default' && typeof roles[role] === 'undefined') {
         useEffect(() => setRole('default'));
-        return;
+        return <></>;
     }
 
     const v = (id: string) => I32ToU32(id === 'default' ? server.default_permissions : roles[id].permissions)
-- 
GitLab