From d94b64dcefcdf4ad29e8d5f8240389d9506968e5 Mon Sep 17 00:00:00 2001
From: Michael Howell <michael@notriddle.com>
Date: Mon, 13 Jan 2025 15:20:36 -0700
Subject: [PATCH] rustdoc: add nobuild typescript checking to our JS

By nobuild, I mean that the type annotations are all in comments,
not in the "native" typescript syntax. This is a bit uglier,
but it lets you rapid-prototype without tsc, works with all
the native browser debugging tools, and keeps Node out of Rust's
bootstrap chain.

This pull request mostly just adds ts-ignore annotations
and type declarations. To actually take good advantage of
typescript, we'll want to "burn down" this pile of unsafe code
until we eventually have a version with almost none of these.

This PR also adds tsc to the mingw-check Dockerfile, so that
it can't fall out of date like the Closure annotations did.

https://rust-lang.zulipchat.com/#narrow/channel/266220-t-rustdoc/topic/typescript
---
 .../docker/host-x86_64/mingw-check/Dockerfile |    5 +-
 src/librustdoc/html/static/js/README.md       |   10 +-
 src/librustdoc/html/static/js/externs.js      |  270 ----
 src/librustdoc/html/static/js/main.js         |  345 ++++-
 src/librustdoc/html/static/js/rustdoc.d.ts    |  387 ++++++
 .../html/static/js/scrape-examples.js         |    3 +
 src/librustdoc/html/static/js/search.js       | 1201 ++++++++++++-----
 src/librustdoc/html/static/js/settings.js     |    3 +
 src/librustdoc/html/static/js/src-script.js   |    3 +
 src/librustdoc/html/static/js/storage.js      |   98 +-
 src/librustdoc/html/static/js/tsconfig.json   |   15 +
 11 files changed, 1658 insertions(+), 682 deletions(-)
 delete mode 100644 src/librustdoc/html/static/js/externs.js
 create mode 100644 src/librustdoc/html/static/js/rustdoc.d.ts
 create mode 100644 src/librustdoc/html/static/js/tsconfig.json

diff --git a/src/ci/docker/host-x86_64/mingw-check/Dockerfile b/src/ci/docker/host-x86_64/mingw-check/Dockerfile
index d408cd518a00b..9234c6dc921ee 100644
--- a/src/ci/docker/host-x86_64/mingw-check/Dockerfile
+++ b/src/ci/docker/host-x86_64/mingw-check/Dockerfile
@@ -29,7 +29,7 @@ ENV PATH="/node/bin:${PATH}"
 
 # Install es-check
 # Pin its version to prevent unrelated CI failures due to future es-check versions.
-RUN npm install es-check@6.1.1 eslint@8.6.0 -g
+RUN npm install es-check@6.1.1 eslint@8.6.0 typescript@5.7.3 -g
 
 COPY scripts/sccache.sh /scripts/
 RUN sh /scripts/sccache.sh
@@ -68,4 +68,5 @@ ENV SCRIPT \
            es-check es2019 ../src/librustdoc/html/static/js/*.js && \
            eslint -c ../src/librustdoc/html/static/.eslintrc.js ../src/librustdoc/html/static/js/*.js && \
            eslint -c ../src/tools/rustdoc-js/.eslintrc.js ../src/tools/rustdoc-js/tester.js && \
-           eslint -c ../src/tools/rustdoc-gui/.eslintrc.js ../src/tools/rustdoc-gui/tester.js
+           eslint -c ../src/tools/rustdoc-gui/.eslintrc.js ../src/tools/rustdoc-gui/tester.js && \
+           tsc --project ../src/librustdoc/html/static/js/tsconfig.json
diff --git a/src/librustdoc/html/static/js/README.md b/src/librustdoc/html/static/js/README.md
index 1fd859ad7cf49..e99d7330f0ed8 100644
--- a/src/librustdoc/html/static/js/README.md
+++ b/src/librustdoc/html/static/js/README.md
@@ -3,13 +3,9 @@
 These JavaScript files are incorporated into the rustdoc binary at build time,
 and are minified and written to the filesystem as part of the doc build process.
 
-We use the [Closure Compiler](https://github.com/google/closure-compiler/wiki/Annotating-JavaScript-for-the-Closure-Compiler)
+We use the [TypeScript Compiler](https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html)
 dialect of JSDoc to comment our code and annotate params and return types.
 To run a check:
 
-    ./x.py doc library/std
-    npm i -g google-closure-compiler
-    google-closure-compiler -W VERBOSE \
-      build/<YOUR PLATFORM>/doc/{search-index*.js,crates*.js} \
-      src/librustdoc/html/static/js/{search.js,main.js,storage.js} \
-      --externs src/librustdoc/html/static/js/externs.js >/dev/null
+    npm i -g typescript
+    tsc --project tsconfig.json
diff --git a/src/librustdoc/html/static/js/externs.js b/src/librustdoc/html/static/js/externs.js
deleted file mode 100644
index c4faca1c0c3bc..0000000000000
--- a/src/librustdoc/html/static/js/externs.js
+++ /dev/null
@@ -1,270 +0,0 @@
-// This file contains type definitions that are processed by the Closure Compiler but are
-// not put into the JavaScript we include as part of the documentation. It is used for
-// type checking. See README.md in this directory for more info.
-
-/* eslint-disable */
-let searchState;
-function initSearch(searchIndex){}
-
-/**
- * @typedef {{
- *     name: string,
- *     id: number|null,
- *     fullPath: Array<string>,
- *     pathWithoutLast: Array<string>,
- *     pathLast: string,
- *     generics: Array<QueryElement>,
- *     bindings: Map<number, Array<QueryElement>>,
- * }}
- */
-let QueryElement;
-
-/**
- * @typedef {{
- *      pos: number,
- *      totalElems: number,
- *      typeFilter: (null|string),
- *      userQuery: string,
- *      isInBinding: (null|string),
- * }}
- */
-let ParserState;
-
-/**
- * @typedef {{
- *     original: string,
- *     userQuery: string,
- *     typeFilter: number,
- *     elems: Array<QueryElement>,
- *     args: Array<QueryElement>,
- *     returned: Array<QueryElement>,
- *     foundElems: number,
- *     totalElems: number,
- *     literalSearch: boolean,
- *     hasReturnArrow: boolean,
- *     corrections: Array<{from: string, to: integer}> | null,
- *     typeFingerprint: Uint32Array,
- *     error: Array<string> | null,
- * }}
- */
-let ParsedQuery;
-
-/**
- * @typedef {{
- *    crate: string,
- *    desc: string,
- *    id: number,
- *    name: string,
- *    normalizedName: string,
- *    parent: (Object|null|undefined),
- *    path: string,
- *    ty: (Number|null|number),
- *    type: FunctionSearchType?
- * }}
- */
-let Row;
-
-/**
- * @typedef {{
- *    in_args: Array<Object>,
- *    returned: Array<Object>,
- *    others: Array<Object>,
- *    query: ParsedQuery,
- * }}
- */
-let ResultsTable;
-
-/**
- * @typedef {Map<String, ResultObject>}
- */
-let Results;
-
-/**
- * @typedef {{
- *     desc: string,
- *     displayPath: string,
- *     fullPath: string,
- *     href: string,
- *     id: number,
- *     lev: number,
- *     name: string,
- *     normalizedName: string,
- *     parent: (Object|undefined),
- *     path: string,
- *     ty: number,
- *     type: FunctionSearchType?,
- *     displayType: Promise<Array<Array<string>>>|null,
- *     displayTypeMappedNames: Promise<Array<[string, Array<string>]>>|null,
- * }}
- */
-let ResultObject;
-
-/**
- * A pair of [inputs, outputs], or 0 for null. This is stored in the search index.
- * The JavaScript deserializes this into FunctionSearchType.
- *
- * Numeric IDs are *ONE-indexed* into the paths array (`p`). Zero is used as a sentinel for `null`
- * because `null` is four bytes while `0` is one byte.
- *
- * An input or output can be encoded as just a number if there is only one of them, AND
- * it has no generics. The no generics rule exists to avoid ambiguity: imagine if you had
- * a function with a single output, and that output had a single generic:
- *
- *     fn something() -> Result<usize, usize>
- *
- * If output was allowed to be any RawFunctionType, it would look like thi
- *
- *     [[], [50, [3, 3]]]
- *
- * The problem is that the above output could be interpreted as either a type with ID 50 and two
- * generics, or it could be interpreted as a pair of types, the first one with ID 50 and the second
- * with ID 3 and a single generic parameter that is also ID 3. We avoid this ambiguity by choosing
- * in favor of the pair of types interpretation. This is why the `(number|Array<RawFunctionType>)`
- * is used instead of `(RawFunctionType|Array<RawFunctionType>)`.
- *
- * The output can be skipped if it's actually unit and there's no type constraints. If thi
- * function accepts constrained generics, then the output will be unconditionally emitted, and
- * after it will come a list of trait constraints. The position of the item in the list will
- * determine which type parameter it is. For example:
- *
- *     [1, 2, 3, 4, 5]
- *      ^  ^  ^  ^  ^
- *      |  |  |  |  - generic parameter (-3) of trait 5
- *      |  |  |  - generic parameter (-2) of trait 4
- *      |  |  - generic parameter (-1) of trait 3
- *      |  - this function returns a single value (type 2)
- *      - this function takes a single input parameter (type 1)
- *
- * Or, for a less contrived version:
- *
- *     [[[4, -1], 3], [[5, -1]], 11]
- *      -^^^^^^^----   ^^^^^^^   ^^
- *       |        |    |          - generic parameter, roughly `where -1: 11`
- *       |        |    |            since -1 is the type parameter and 11 the trait
- *       |        |    - function output 5<-1>
- *       |        - the overall function signature is something like
- *       |          `fn(4<-1>, 3) -> 5<-1> where -1: 11`
- *       - function input, corresponds roughly to 4<-1>
- *         4 is an index into the `p` array for a type
- *         -1 is the generic parameter, given by 11
- *
- * If a generic parameter has multiple trait constraints, it gets wrapped in an array, just like
- * function inputs and outputs:
- *
- *     [-1, -1, [4, 3]]
- *              ^^^^^^ where -1: 4 + 3
- *
- * If a generic parameter's trait constraint has generic parameters, it gets wrapped in the array
- * even if only one exists. In other words, the ambiguity of `4<3>` and `4 + 3` is resolved in
- * favor of `4 + 3`:
- *
- *     [-1, -1, [[4, 3]]]
- *              ^^^^^^^^ where -1: 4 + 3
- *
- *     [-1, -1, [5, [4, 3]]]
- *              ^^^^^^^^^^^ where -1: 5, -2: 4 + 3
- *
- * If a generic parameter has no trait constraints (like in Rust, the `Sized` constraint i
- * implied and a fake `?Sized` constraint used to note its absence), it will be filled in with 0.
- *
- * @typedef {(
- *     0 |
- *     [(number|Array<RawFunctionType>)] |
- *     [(number|Array<RawFunctionType>), (number|Array<RawFunctionType>)] |
- *     Array<(number|Array<RawFunctionType>)>
- * )}
- */
-let RawFunctionSearchType;
-
-/**
- * A single function input or output type. This is either a single path ID, or a pair of
- * [path ID, generics].
- *
- * Numeric IDs are *ONE-indexed* into the paths array (`p`). Zero is used as a sentinel for `null`
- * because `null` is four bytes while `0` is one byte.
- *
- * @typedef {number | [number, Array<RawFunctionType>]}
- */
-let RawFunctionType;
-
-/**
- * @typedef {{
- *     inputs: Array<FunctionType>,
- *     output: Array<FunctionType>,
- *     where_clause: Array<Array<FunctionType>>,
- * }}
- */
-let FunctionSearchType;
-
-/**
- * @typedef {{
- *     id: (null|number),
- *     ty: number,
- *     generics: Array<FunctionType>,
- *     bindings: Map<integer, Array<FunctionType>>,
- * }}
- */
-let FunctionType;
-
-/**
- * The raw search data for a given crate. `n`, `t`, `d`, `i`, and `f`
- * are arrays with the same length. `q`, `a`, and `c` use a sparse
- * representation for compactness.
- *
- * `n[i]` contains the name of an item.
- *
- * `t[i]` contains the type of that item
- * (as a string of characters that represent an offset in `itemTypes`).
- *
- * `d[i]` contains the description of that item.
- *
- * `q` contains the full paths of the items. For compactness, it is a set of
- * (index, path) pairs used to create a map. If a given index `i` is
- * not present, this indicates "same as the last index present".
- *
- * `i[i]` contains an item's parent, usually a module. For compactness,
- * it is a set of indexes into the `p` array.
- *
- * `f` contains function signatures, or `0` if the item isn't a function.
- * More information on how they're encoded can be found in rustc-dev-guide
- *
- * Functions are themselves encoded as arrays. The first item is a list of
- * types representing the function's inputs, and the second list item is a list
- * of types representing the function's output. Tuples are flattened.
- * Types are also represented as arrays; the first item is an index into the `p`
- * array, while the second is a list of types representing any generic parameters.
- *
- * b[i] contains an item's impl disambiguator. This is only present if an item
- * is defined in an impl block and, the impl block's type has more than one associated
- * item with the same name.
- *
- * `a` defines aliases with an Array of pairs: [name, offset], where `offset`
- * points into the n/t/d/q/i/f arrays.
- *
- * `doc` contains the description of the crate.
- *
- * `p` is a list of path/type pairs. It is used for parents and function parameters.
- * The first item is the type, the second is the name, the third is the visible path (if any) and
- * the fourth is the canonical path used for deduplication (if any).
- *
- * `r` is the canonical path used for deduplication of re-exported items.
- * It is not used for associated items like methods (that's the fourth element
- * of `p`) but is used for modules items like free functions.
- *
- * `c` is an array of item indices that are deprecated.
- * @typedef {{
- *   doc: string,
- *   a: Object,
- *   n: Array<string>,
- *   t: string,
- *   d: Array<string>,
- *   q: Array<[number, string]>,
- *   i: Array<number>,
- *   f: string,
- *   p: Array<[number, string] | [number, string, number] | [number, string, number, number]>,
- *   b: Array<[number, String]>,
- *   c: Array<number>,
- *   r: Array<[number, number]>,
- * }}
- */
-let RawSearchIndexCrate;
diff --git a/src/librustdoc/html/static/js/main.js b/src/librustdoc/html/static/js/main.js
index 984b0877d8de2..ccf4002bb300d 100644
--- a/src/librustdoc/html/static/js/main.js
+++ b/src/librustdoc/html/static/js/main.js
@@ -11,8 +11,13 @@
 window.RUSTDOC_TOOLTIP_HOVER_MS = 300;
 window.RUSTDOC_TOOLTIP_HOVER_EXIT_MS = 450;
 
-// Given a basename (e.g. "storage") and an extension (e.g. ".js"), return a URL
-// for a resource under the root-path, with the resource-suffix.
+/**
+ * Given a basename (e.g. "storage") and an extension (e.g. ".js"), return a URL
+ * for a resource under the root-path, with the resource-suffix.
+ *
+ * @param {string} basename
+ * @param {string} extension
+ */
 function resourcePath(basename, extension) {
     return getVar("root-path") + basename + getVar("resource-suffix") + extension;
 }
@@ -27,13 +32,18 @@ function hideMain() {
 
 function showMain() {
     const main = document.getElementById(MAIN_ID);
+    if (!main) {
+        return;
+    }
     removeClass(main, "hidden");
     const mainHeading = main.querySelector(".main-heading");
-    if (mainHeading && searchState.rustdocToolbar) {
-        if (searchState.rustdocToolbar.parentElement) {
-            searchState.rustdocToolbar.parentElement.removeChild(searchState.rustdocToolbar);
+    if (mainHeading && window.searchState.rustdocToolbar) {
+        if (window.searchState.rustdocToolbar.parentElement) {
+            window.searchState.rustdocToolbar.parentElement.removeChild(
+                window.searchState.rustdocToolbar,
+            );
         }
-        mainHeading.appendChild(searchState.rustdocToolbar);
+        mainHeading.appendChild(window.searchState.rustdocToolbar);
     }
     const toggle = document.getElementById("toggle-all-docs");
     if (toggle) {
@@ -61,16 +71,20 @@ function setMobileTopbar() {
     }
 }
 
-// Gets the human-readable string for the virtual-key code of the
-// given KeyboardEvent, ev.
-//
-// This function is meant as a polyfill for KeyboardEvent#key,
-// since it is not supported in IE 11 or Chrome for Android. We also test for
-// KeyboardEvent#keyCode because the handleShortcut handler is
-// also registered for the keydown event, because Blink doesn't fire
-// keypress on hitting the Escape key.
-//
-// So I guess you could say things are getting pretty interoperable.
+/**
+ * Gets the human-readable string for the virtual-key code of the
+ * given KeyboardEvent, ev.
+ *
+ * This function is meant as a polyfill for KeyboardEvent#key,
+ * since it is not supported in IE 11 or Chrome for Android. We also test for
+ * KeyboardEvent#keyCode because the handleShortcut handler is
+ * also registered for the keydown event, because Blink doesn't fire
+ * keypress on hitting the Escape key.
+ *
+ * So I guess you could say things are getting pretty interoperable.
+ *
+ * @param {KeyboardEvent} ev
+ */
 function getVirtualKey(ev) {
     if ("key" in ev && typeof ev.key !== "undefined") {
         return ev.key;
@@ -110,6 +124,9 @@ function getNakedUrl() {
  * @param {HTMLElement} referenceNode
  */
 function insertAfter(newNode, referenceNode) {
+    // You're not allowed to pass an element with no parent.
+    // I dunno how to make TS's typechecker see that.
+    // @ts-expect-error
     referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
 }
 
@@ -129,6 +146,7 @@ function getOrCreateSection(id, classes) {
         el = document.createElement("section");
         el.id = id;
         el.className = classes;
+        // @ts-expect-error
         insertAfter(el, document.getElementById(MAIN_ID));
     }
     return el;
@@ -159,12 +177,13 @@ function getNotDisplayedElem() {
  * contains the displayed element (there can be only one at the same time!). So basically, we switch
  * elements between the two `<section>` elements.
  *
- * @param {HTMLElement} elemToDisplay
+ * @param {HTMLElement|null} elemToDisplay
  */
 function switchDisplayedElement(elemToDisplay) {
     const el = getAlternativeDisplayElem();
 
     if (el.children.length > 0) {
+        // @ts-expect-error
         getNotDisplayedElem().appendChild(el.firstElementChild);
     }
     if (elemToDisplay === null) {
@@ -177,10 +196,14 @@ function switchDisplayedElement(elemToDisplay) {
     removeClass(el, "hidden");
 
     const mainHeading = elemToDisplay.querySelector(".main-heading");
+    // @ts-expect-error
     if (mainHeading && searchState.rustdocToolbar) {
+        // @ts-expect-error
         if (searchState.rustdocToolbar.parentElement) {
+            // @ts-expect-error
             searchState.rustdocToolbar.parentElement.removeChild(searchState.rustdocToolbar);
         }
+        // @ts-expect-error
         mainHeading.appendChild(searchState.rustdocToolbar);
     }
 }
@@ -189,6 +212,7 @@ function browserSupportsHistoryApi() {
     return window.history && typeof window.history.pushState === "function";
 }
 
+// @ts-expect-error
 function preLoadCss(cssUrl) {
     // https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preload
     const link = document.createElement("link");
@@ -201,6 +225,7 @@ function preLoadCss(cssUrl) {
 (function() {
     const isHelpPage = window.location.pathname.endsWith("/help.html");
 
+    // @ts-expect-error
     function loadScript(url, errorCallback) {
         const script = document.createElement("script");
         script.src = url;
@@ -211,21 +236,25 @@ function preLoadCss(cssUrl) {
     }
 
     if (getSettingsButton()) {
+        // @ts-expect-error
         getSettingsButton().onclick = event => {
             if (event.ctrlKey || event.altKey || event.metaKey) {
                 return;
             }
+            // @ts-expect-error
             window.hideAllModals(false);
             addClass(getSettingsButton(), "rotate");
             event.preventDefault();
             // Sending request for the CSS and the JS files at the same time so it will
             // hopefully be loaded when the JS will generate the settings content.
+            // @ts-expect-error
             loadScript(getVar("static-root-path") + getVar("settings-js"));
             // Pre-load all theme CSS files, so that switching feels seamless.
             //
             // When loading settings.html as a standalone page, the equivalent HTML is
             // generated in context.rs.
             setTimeout(() => {
+                // @ts-expect-error
                 const themes = getVar("themes").split(",");
                 for (const theme of themes) {
                     // if there are no themes, do nothing
@@ -241,6 +270,8 @@ function preLoadCss(cssUrl) {
     window.searchState = {
         rustdocToolbar: document.querySelector("rustdoc-toolbar"),
         loadingText: "Loading search results...",
+        // This will always be an HTMLInputElement, but tsc can't see that
+        // @ts-expect-error
         input: document.getElementsByClassName("search-input")[0],
         outputElement: () => {
             let el = document.getElementById("search");
@@ -263,31 +294,38 @@ function preLoadCss(cssUrl) {
         // tab and back preserves the element that was focused.
         focusedByTab: [null, null, null],
         clearInputTimeout: () => {
-            if (searchState.timeout !== null) {
-                clearTimeout(searchState.timeout);
-                searchState.timeout = null;
+            if (window.searchState.timeout !== null) {
+                clearTimeout(window.searchState.timeout);
+                window.searchState.timeout = null;
             }
         },
-        isDisplayed: () => searchState.outputElement().parentElement.id === ALTERNATIVE_DISPLAY_ID,
+        // @ts-expect-error
+        isDisplayed: () => {
+            const outputElement = window.searchState.outputElement();
+            return outputElement &&
+                outputElement.parentElement &&
+                outputElement.parentElement.id === ALTERNATIVE_DISPLAY_ID;
+        },
         // Sets the focus on the search bar at the top of the page
         focus: () => {
-            searchState.input.focus();
+            window.searchState.input && window.searchState.input.focus();
         },
         // Removes the focus from the search bar.
         defocus: () => {
-            searchState.input.blur();
+            window.searchState.input && window.searchState.input.blur();
         },
         showResults: search => {
             if (search === null || typeof search === "undefined") {
-                search = searchState.outputElement();
+                search = window.searchState.outputElement();
             }
             switchDisplayedElement(search);
-            searchState.mouseMovedAfterSearch = false;
-            document.title = searchState.title;
+            // @ts-expect-error
+            window.searchState.mouseMovedAfterSearch = false;
+            document.title = window.searchState.title;
         },
         removeQueryParameters: () => {
             // We change the document title.
-            document.title = searchState.titleBeforeSearch;
+            document.title = window.searchState.titleBeforeSearch;
             if (browserSupportsHistoryApi()) {
                 history.replaceState(null, "", getNakedUrl() + window.location.hash);
             }
@@ -295,9 +333,10 @@ function preLoadCss(cssUrl) {
         hideResults: () => {
             switchDisplayedElement(null);
             // We also remove the query parameter from the URL.
-            searchState.removeQueryParameters();
+            window.searchState.removeQueryParameters();
         },
         getQueryStringParams: () => {
+            /** @type {Object.<any, string>} */
             const params = {};
             window.location.search.substring(1).split("&").
                 map(s => {
@@ -309,26 +348,28 @@ function preLoadCss(cssUrl) {
             return params;
         },
         setup: () => {
-            const search_input = searchState.input;
-            if (!searchState.input) {
+            const search_input = window.searchState.input;
+            if (!search_input) {
                 return;
             }
             let searchLoaded = false;
             // If you're browsing the nightly docs, the page might need to be refreshed for the
             // search to work because the hash of the JS scripts might have changed.
             function sendSearchForm() {
+                // @ts-expect-error
                 document.getElementsByClassName("search-form")[0].submit();
             }
             function loadSearch() {
                 if (!searchLoaded) {
                     searchLoaded = true;
+                    // @ts-expect-error
                     loadScript(getVar("static-root-path") + getVar("search-js"), sendSearchForm);
                     loadScript(resourcePath("search-index", ".js"), sendSearchForm);
                 }
             }
 
             search_input.addEventListener("focus", () => {
-                search_input.origPlaceholder = search_input.placeholder;
+                window.searchState.origPlaceholder = search_input.placeholder;
                 search_input.placeholder = "Type your search here.";
                 loadSearch();
             });
@@ -337,16 +378,21 @@ function preLoadCss(cssUrl) {
                 loadSearch();
             }
 
-            const params = searchState.getQueryStringParams();
+            const params = window.searchState.getQueryStringParams();
             if (params.search !== undefined) {
-                searchState.setLoadingSearch();
+                window.searchState.setLoadingSearch();
                 loadSearch();
             }
         },
         setLoadingSearch: () => {
-            const search = searchState.outputElement();
-            search.innerHTML = "<h3 class=\"search-loading\">" + searchState.loadingText + "</h3>";
-            searchState.showResults(search);
+            const search = window.searchState.outputElement();
+            if (!search) {
+                return;
+            }
+            search.innerHTML = "<h3 class=\"search-loading\">" +
+                window.searchState.loadingText +
+                "</h3>";
+            window.searchState.showResults(search);
         },
         descShards: new Map(),
         loadDesc: async function({descShard, descIndex}) {
@@ -370,6 +416,8 @@ function preLoadCss(cssUrl) {
             return list[descIndex];
         },
         loadedDescShard: function(crate, shard, data) {
+            // If loadedDescShard gets called, then the library must have been declared.
+            // @ts-expect-error
             this.descShards.get(crate)[shard].resolve(data.split("\n"));
         },
     };
@@ -377,8 +425,11 @@ function preLoadCss(cssUrl) {
     const toggleAllDocsId = "toggle-all-docs";
     let savedHash = "";
 
+    /**
+     * @param {HashChangeEvent|null} ev
+     */
     function handleHashes(ev) {
-        if (ev !== null && searchState.isDisplayed() && ev.newURL) {
+        if (ev !== null && window.searchState.isDisplayed() && ev.newURL) {
             // This block occurs when clicking on an element in the navbar while
             // in a search.
             switchDisplayedElement(null);
@@ -419,6 +470,7 @@ function preLoadCss(cssUrl) {
                     }
                     return onEachLazy(implElem.parentElement.parentElement.querySelectorAll(
                         `[id^="${assocId}"]`),
+                        // @ts-expect-error
                         item => {
                             const numbered = /^(.+?)-([0-9]+)$/.exec(item.id);
                             if (item.id === assocId || (numbered && numbered[1] === assocId)) {
@@ -437,12 +489,16 @@ function preLoadCss(cssUrl) {
         }
     }
 
+    /**
+     * @param {HashChangeEvent|null} ev
+     */
     function onHashChange(ev) {
         // If we're in mobile mode, we should hide the sidebar in any case.
         hideSidebar();
         handleHashes(ev);
     }
 
+    // @ts-expect-error
     function openParentDetails(elem) {
         while (elem) {
             if (elem.tagName === "DETAILS") {
@@ -452,18 +508,25 @@ function preLoadCss(cssUrl) {
         }
     }
 
+    // @ts-expect-error
     function expandSection(id) {
         openParentDetails(document.getElementById(id));
     }
 
+    // @ts-expect-error
     function handleEscape(ev) {
+        // @ts-expect-error
         searchState.clearInputTimeout();
+        // @ts-expect-error
         searchState.hideResults();
         ev.preventDefault();
+        // @ts-expect-error
         searchState.defocus();
+        // @ts-expect-error
         window.hideAllModals(true); // true = reset focus for tooltips
     }
 
+    // @ts-expect-error
     function handleShortcut(ev) {
         // Don't interfere with browser shortcuts
         const disableShortcuts = getSettingValue("disable-shortcuts") === "true";
@@ -471,8 +534,11 @@ function preLoadCss(cssUrl) {
             return;
         }
 
+        // @ts-expect-error
         if (document.activeElement.tagName === "INPUT" &&
+            // @ts-expect-error
             document.activeElement.type !== "checkbox" &&
+            // @ts-expect-error
             document.activeElement.type !== "radio") {
             switch (getVirtualKey(ev)) {
             case "Escape":
@@ -489,6 +555,7 @@ function preLoadCss(cssUrl) {
             case "S":
             case "/":
                 ev.preventDefault();
+                // @ts-expect-error
                 searchState.focus();
                 break;
 
@@ -515,6 +582,7 @@ function preLoadCss(cssUrl) {
     document.addEventListener("keydown", handleShortcut);
 
     function addSidebarItems() {
+        // @ts-expect-error
         if (!window.SIDEBAR_ITEMS) {
             return;
         }
@@ -529,6 +597,7 @@ function preLoadCss(cssUrl) {
          *                          "Modules", or "Macros".
          */
         function block(shortty, id, longty) {
+            // @ts-expect-error
             const filtered = window.SIDEBAR_ITEMS[shortty];
             if (!filtered) {
                 return;
@@ -564,7 +633,9 @@ function preLoadCss(cssUrl) {
                 li.appendChild(link);
                 ul.appendChild(li);
             }
+            // @ts-expect-error
             sidebar.appendChild(h3);
+            // @ts-expect-error
             sidebar.appendChild(ul);
         }
 
@@ -600,6 +671,7 @@ function preLoadCss(cssUrl) {
     }
 
     // <https://github.com/search?q=repo%3Arust-lang%2Frust+[RUSTDOCIMPL]+trait.impl&type=code>
+    // @ts-expect-error
     window.register_implementors = imp => {
         const implementors = document.getElementById("implementors-list");
         const synthetic_implementors = document.getElementById("synthetic-implementors-list");
@@ -615,18 +687,22 @@ function preLoadCss(cssUrl) {
             //
             // By the way, this is only used by and useful for traits implemented automatically
             // (like "Send" and "Sync").
+            // @ts-expect-error
             onEachLazy(synthetic_implementors.getElementsByClassName("impl"), el => {
                 const aliases = el.getAttribute("data-aliases");
                 if (!aliases) {
                     return;
                 }
+                // @ts-expect-error
                 aliases.split(",").forEach(alias => {
                     inlined_types.add(alias);
                 });
             });
         }
 
+        // @ts-expect-error
         let currentNbImpls = implementors.getElementsByClassName("impl").length;
+        // @ts-expect-error
         const traitName = document.querySelector(".main-heading h1 > .trait").textContent;
         const baseIdName = "impl-" + traitName + "-";
         const libs = Object.getOwnPropertyNames(imp);
@@ -636,6 +712,7 @@ function preLoadCss(cssUrl) {
         const script = document
             .querySelector("script[data-ignore-extern-crates]");
         const ignoreExternCrates = new Set(
+            // @ts-expect-error
             (script ? script.getAttribute("data-ignore-extern-crates") : "").split(","),
         );
         for (const lib of libs) {
@@ -663,6 +740,7 @@ function preLoadCss(cssUrl) {
                 code.innerHTML = struct[TEXT_IDX];
                 addClass(code, "code-header");
 
+                // @ts-expect-error
                 onEachLazy(code.getElementsByTagName("a"), elem => {
                     const href = elem.getAttribute("href");
 
@@ -681,12 +759,15 @@ function preLoadCss(cssUrl) {
                 addClass(display, "impl");
                 display.appendChild(anchor);
                 display.appendChild(code);
+                // @ts-expect-error
                 list.appendChild(display);
                 currentNbImpls += 1;
             }
         }
     };
+    // @ts-expect-error
     if (window.pending_implementors) {
+        // @ts-expect-error
         window.register_implementors(window.pending_implementors);
     }
 
@@ -719,12 +800,15 @@ function preLoadCss(cssUrl) {
      *
      * - After processing all of the impls, it sorts the sidebar items by name.
      *
-     * @param {{[cratename: string]: Array<Array<string|0>>}} impl
+     * @param {{[cratename: string]: Array<Array<string|0>>}} imp
      */
+    // @ts-expect-error
     window.register_type_impls = imp => {
+        // @ts-expect-error
         if (!imp || !imp[window.currentCrate]) {
             return;
         }
+        // @ts-expect-error
         window.pending_type_impls = null;
         const idMap = new Map();
 
@@ -744,6 +828,7 @@ function preLoadCss(cssUrl) {
         let associatedConstants = document.querySelector(".sidebar .block.associatedconstant");
         let sidebarTraitList = document.querySelector(".sidebar .block.trait-implementation");
 
+        // @ts-expect-error
         for (const impList of imp[window.currentCrate]) {
             const types = impList.slice(2);
             const text = impList[0];
@@ -772,20 +857,28 @@ function preLoadCss(cssUrl) {
                     h.appendChild(link);
                     trait_implementations = outputList;
                     trait_implementations_header = outputListHeader;
+                    // @ts-expect-error
                     sidebarSection.appendChild(h);
                     sidebarTraitList = document.createElement("ul");
                     sidebarTraitList.className = "block trait-implementation";
+                    // @ts-expect-error
                     sidebarSection.appendChild(sidebarTraitList);
+                    // @ts-expect-error
                     mainContent.appendChild(outputListHeader);
+                    // @ts-expect-error
                     mainContent.appendChild(outputList);
                 } else {
                     implementations = outputList;
                     if (trait_implementations) {
+                        // @ts-expect-error
                         mainContent.insertBefore(outputListHeader, trait_implementations_header);
+                        // @ts-expect-error
                         mainContent.insertBefore(outputList, trait_implementations_header);
                     } else {
                         const mainContent = document.querySelector("#main-content");
+                        // @ts-expect-error
                         mainContent.appendChild(outputListHeader);
+                        // @ts-expect-error
                         mainContent.appendChild(outputList);
                     }
                 }
@@ -793,6 +886,7 @@ function preLoadCss(cssUrl) {
             const template = document.createElement("template");
             template.innerHTML = text;
 
+            // @ts-expect-error
             onEachLazy(template.content.querySelectorAll("a"), elem => {
                 const href = elem.getAttribute("href");
 
@@ -800,6 +894,7 @@ function preLoadCss(cssUrl) {
                     elem.setAttribute("href", window.rootPath + href);
                 }
             });
+            // @ts-expect-error
             onEachLazy(template.content.querySelectorAll("[id]"), el => {
                 let i = 0;
                 if (idMap.has(el.id)) {
@@ -817,6 +912,7 @@ function preLoadCss(cssUrl) {
                     const oldHref = `#${el.id}`;
                     const newHref = `#${el.id}-${i}`;
                     el.id = `${el.id}-${i}`;
+                    // @ts-expect-error
                     onEachLazy(template.content.querySelectorAll("a[href]"), link => {
                         if (link.getAttribute("href") === oldHref) {
                             link.href = newHref;
@@ -830,11 +926,14 @@ function preLoadCss(cssUrl) {
             if (isTrait) {
                 const li = document.createElement("li");
                 const a = document.createElement("a");
+                // @ts-expect-error
                 a.href = `#${template.content.querySelector(".impl").id}`;
                 a.textContent = traitName;
                 li.appendChild(a);
+                // @ts-expect-error
                 sidebarTraitList.append(li);
             } else {
+                // @ts-expect-error
                 onEachLazy(templateAssocItems, item => {
                     let block = hasClass(item, "associatedtype") ? associatedTypes : (
                         hasClass(item, "associatedconstant") ? associatedConstants : (
@@ -856,10 +955,14 @@ function preLoadCss(cssUrl) {
                         const insertionReference = methods || sidebarTraitList;
                         if (insertionReference) {
                             const insertionReferenceH = insertionReference.previousElementSibling;
+                            // @ts-expect-error
                             sidebarSection.insertBefore(blockHeader, insertionReferenceH);
+                            // @ts-expect-error
                             sidebarSection.insertBefore(block, insertionReferenceH);
                         } else {
+                            // @ts-expect-error
                             sidebarSection.appendChild(blockHeader);
+                            // @ts-expect-error
                             sidebarSection.appendChild(block);
                         }
                         if (hasClass(item, "associatedtype")) {
@@ -896,11 +999,14 @@ function preLoadCss(cssUrl) {
             list.replaceChildren(...newChildren);
         }
     };
+    // @ts-expect-error
     if (window.pending_type_impls) {
+        // @ts-expect-error
         window.register_type_impls(window.pending_type_impls);
     }
 
     function addSidebarCrates() {
+        // @ts-expect-error
         if (!window.ALL_CRATES) {
             return;
         }
@@ -914,6 +1020,7 @@ function preLoadCss(cssUrl) {
         const ul = document.createElement("ul");
         ul.className = "block crate";
 
+        // @ts-expect-error
         for (const crate of window.ALL_CRATES) {
             const link = document.createElement("a");
             link.href = window.rootPath + crate + "/index.html";
@@ -933,17 +1040,20 @@ function preLoadCss(cssUrl) {
     function expandAllDocs() {
         const innerToggle = document.getElementById(toggleAllDocsId);
         removeClass(innerToggle, "will-expand");
+        // @ts-expect-error
         onEachLazy(document.getElementsByClassName("toggle"), e => {
             if (!hasClass(e, "type-contents-toggle") && !hasClass(e, "more-examples-toggle")) {
                 e.open = true;
             }
         });
+        // @ts-expect-error
         innerToggle.children[0].innerText = "Summary";
     }
 
     function collapseAllDocs() {
         const innerToggle = document.getElementById(toggleAllDocsId);
         addClass(innerToggle, "will-expand");
+        // @ts-expect-error
         onEachLazy(document.getElementsByClassName("toggle"), e => {
             if (e.parentNode.id !== "implementations-list" ||
                 (!hasClass(e, "implementors-toggle") &&
@@ -952,6 +1062,7 @@ function preLoadCss(cssUrl) {
                 e.open = false;
             }
         });
+        // @ts-expect-error
         innerToggle.children[0].innerText = "Show all";
     }
 
@@ -977,9 +1088,11 @@ function preLoadCss(cssUrl) {
         const hideImplementations = getSettingValue("auto-hide-trait-implementations") === "true";
         const hideLargeItemContents = getSettingValue("auto-hide-large-items") !== "false";
 
+        // @ts-expect-error
         function setImplementorsTogglesOpen(id, open) {
             const list = document.getElementById(id);
             if (list !== null) {
+                // @ts-expect-error
                 onEachLazy(list.getElementsByClassName("implementors-toggle"), e => {
                     e.open = open;
                 });
@@ -991,6 +1104,7 @@ function preLoadCss(cssUrl) {
             setImplementorsTogglesOpen("blanket-implementations-list", false);
         }
 
+        // @ts-expect-error
         onEachLazy(document.getElementsByClassName("toggle"), e => {
             if (!hideLargeItemContents && hasClass(e, "type-contents-toggle")) {
                 e.open = true;
@@ -1002,6 +1116,7 @@ function preLoadCss(cssUrl) {
         });
     }());
 
+    // @ts-expect-error
     window.rustdoc_add_line_numbers_to_examples = () => {
         if (document.querySelector(".rustdoc.src")) {
             // We are in the source code page, nothing to be done here!
@@ -1009,6 +1124,7 @@ function preLoadCss(cssUrl) {
         }
         onEachLazy(document.querySelectorAll(
             ":not(.scraped-example) > .example-wrap > pre:not(.example-line-numbers)",
+        // @ts-expect-error
         ), x => {
             const parent = x.parentNode;
             const line_numbers = parent.querySelectorAll(".example-line-numbers");
@@ -1027,33 +1143,41 @@ function preLoadCss(cssUrl) {
         });
     };
 
+    // @ts-expect-error
     window.rustdoc_remove_line_numbers_from_examples = () => {
+        // @ts-expect-error
         onEachLazy(document.querySelectorAll(".example-wrap > .example-line-numbers"), x => {
             x.parentNode.removeChild(x);
         });
     };
 
     if (getSettingValue("line-numbers") === "true") {
+        // @ts-expect-error
         window.rustdoc_add_line_numbers_to_examples();
     }
 
     function showSidebar() {
+        // @ts-expect-error
         window.hideAllModals(false);
         const sidebar = document.getElementsByClassName("sidebar")[0];
+        // @ts-expect-error
         addClass(sidebar, "shown");
     }
 
     function hideSidebar() {
         const sidebar = document.getElementsByClassName("sidebar")[0];
+        // @ts-expect-error
         removeClass(sidebar, "shown");
     }
 
     window.addEventListener("resize", () => {
+        // @ts-expect-error
         if (window.CURRENT_TOOLTIP_ELEMENT) {
             // As a workaround to the behavior of `contains: layout` used in doc togglers,
             // tooltip popovers are positioned using javascript.
             //
             // This means when the window is resized, we need to redo the layout.
+            // @ts-expect-error
             const base = window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE;
             const force_visible = base.TOOLTIP_FORCE_VISIBLE;
             hideTooltip(false);
@@ -1069,6 +1193,7 @@ function preLoadCss(cssUrl) {
         mainElem.addEventListener("click", hideSidebar);
     }
 
+    // @ts-expect-error
     onEachLazy(document.querySelectorAll("a[href^='#']"), el => {
         // For clicks on internal links (<A> tags with a hash property), we expand the section we're
         // jumping to *before* jumping there. We can't do this in onHashChange, because it changes
@@ -1079,7 +1204,9 @@ function preLoadCss(cssUrl) {
         });
     });
 
+    // @ts-expect-error
     onEachLazy(document.querySelectorAll(".toggle > summary:not(.hideme)"), el => {
+        // @ts-expect-error
         el.addEventListener("click", e => {
             if (e.target.tagName !== "SUMMARY" && e.target.tagName !== "A") {
                 e.preventDefault();
@@ -1090,15 +1217,17 @@ function preLoadCss(cssUrl) {
     /**
      * Show a tooltip immediately.
      *
-     * @param {DOMElement} e - The tooltip's anchor point. The DOM is consulted to figure
-     *                         out what the tooltip should contain, and where it should be
-     *                         positioned.
+     * @param {HTMLElement} e - The tooltip's anchor point. The DOM is consulted to figure
+     *                          out what the tooltip should contain, and where it should be
+     *                          positioned.
      */
     function showTooltip(e) {
         const notable_ty = e.getAttribute("data-notable-ty");
+        // @ts-expect-error
         if (!window.NOTABLE_TRAITS && notable_ty) {
             const data = document.getElementById("notable-traits-data");
             if (data) {
+                // @ts-expect-error
                 window.NOTABLE_TRAITS = JSON.parse(data.innerText);
             } else {
                 throw new Error("showTooltip() called with notable without any notable traits!");
@@ -1106,36 +1235,44 @@ function preLoadCss(cssUrl) {
         }
         // Make this function idempotent. If the tooltip is already shown, avoid doing extra work
         // and leave it alone.
+        // @ts-expect-error
         if (window.CURRENT_TOOLTIP_ELEMENT && window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE === e) {
+            // @ts-expect-error
             clearTooltipHoverTimeout(window.CURRENT_TOOLTIP_ELEMENT);
             return;
         }
+        // @ts-expect-error
         window.hideAllModals(false);
         const wrapper = document.createElement("div");
         if (notable_ty) {
             wrapper.innerHTML = "<div class=\"content\">" +
+                // @ts-expect-error
                 window.NOTABLE_TRAITS[notable_ty] + "</div>";
         } else {
             // Replace any `title` attribute with `data-title` to avoid double tooltips.
-            if (e.getAttribute("title") !== null) {
-                e.setAttribute("data-title", e.getAttribute("title"));
+            const ttl = e.getAttribute("title");
+            if (ttl !== null) {
+                e.setAttribute("data-title", ttl);
                 e.removeAttribute("title");
             }
-            if (e.getAttribute("data-title") !== null) {
+            const dttl = e.getAttribute("data-title");
+            if (dttl !== null) {
                 const titleContent = document.createElement("div");
                 titleContent.className = "content";
-                titleContent.appendChild(document.createTextNode(e.getAttribute("data-title")));
+                titleContent.appendChild(document.createTextNode(dttl));
                 wrapper.appendChild(titleContent);
             }
         }
         wrapper.className = "tooltip popover";
         const focusCatcher = document.createElement("div");
         focusCatcher.setAttribute("tabindex", "0");
+        // @ts-expect-error
         focusCatcher.onfocus = hideTooltip;
         wrapper.appendChild(focusCatcher);
         const pos = e.getBoundingClientRect();
         // 5px overlap so that the mouse can easily travel from place to place
         wrapper.style.top = (pos.top + window.scrollY + pos.height) + "px";
+        // @ts-expect-error
         wrapper.style.left = 0;
         wrapper.style.right = "auto";
         wrapper.style.visibility = "hidden";
@@ -1152,8 +1289,11 @@ function preLoadCss(cssUrl) {
             );
         }
         wrapper.style.visibility = "";
+        // @ts-expect-error
         window.CURRENT_TOOLTIP_ELEMENT = wrapper;
+        // @ts-expect-error
         window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE = e;
+        // @ts-expect-error
         clearTooltipHoverTimeout(window.CURRENT_TOOLTIP_ELEMENT);
         wrapper.onpointerenter = ev => {
             // If this is a synthetic touch event, ignore it. A click event will be along shortly.
@@ -1164,7 +1304,7 @@ function preLoadCss(cssUrl) {
         };
         wrapper.onpointerleave = ev => {
             // If this is a synthetic touch event, ignore it. A click event will be along shortly.
-            if (ev.pointerType !== "mouse") {
+            if (ev.pointerType !== "mouse" || !(ev.relatedTarget instanceof HTMLElement)) {
                 return;
             }
             if (!e.TOOLTIP_FORCE_VISIBLE && !e.contains(ev.relatedTarget)) {
@@ -1180,23 +1320,27 @@ function preLoadCss(cssUrl) {
      * was called, that timeout gets cleared. If the tooltip is already in the requested state,
      * this function will still clear any pending timeout, but otherwise do nothing.
      *
-     * @param {DOMElement} element - The tooltip's anchor point. The DOM is consulted to figure
-     *                               out what the tooltip should contain, and where it should be
-     *                               positioned.
+     * @param {HTMLElement} element - The tooltip's anchor point. The DOM is consulted to figure
+     *                                out what the tooltip should contain, and where it should be
+     *                                positioned.
      * @param {boolean}    show    - If true, the tooltip will be made visible. If false, it will
      *                               be hidden.
      */
     function setTooltipHoverTimeout(element, show) {
         clearTooltipHoverTimeout(element);
+        // @ts-expect-error
         if (!show && !window.CURRENT_TOOLTIP_ELEMENT) {
             // To "hide" an already hidden element, just cancel its timeout.
             return;
         }
+        // @ts-expect-error
         if (show && window.CURRENT_TOOLTIP_ELEMENT) {
             // To "show" an already visible element, just cancel its timeout.
             return;
         }
+        // @ts-expect-error
         if (window.CURRENT_TOOLTIP_ELEMENT &&
+            // @ts-expect-error
             window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE !== element) {
             // Don't do anything if another tooltip is already visible.
             return;
@@ -1214,22 +1358,29 @@ function preLoadCss(cssUrl) {
      * If a show/hide timeout was set by `setTooltipHoverTimeout`, cancel it. If none exists,
      * do nothing.
      *
-     * @param {DOMElement} element - The tooltip's anchor point,
-     *                               as passed to `setTooltipHoverTimeout`.
+     * @param {HTMLElement} element - The tooltip's anchor point,
+     *                                as passed to `setTooltipHoverTimeout`.
      */
     function clearTooltipHoverTimeout(element) {
         if (element.TOOLTIP_HOVER_TIMEOUT !== undefined) {
+            // @ts-expect-error
             removeClass(window.CURRENT_TOOLTIP_ELEMENT, "fade-out");
             clearTimeout(element.TOOLTIP_HOVER_TIMEOUT);
             delete element.TOOLTIP_HOVER_TIMEOUT;
         }
     }
 
+    // @ts-expect-error
     function tooltipBlurHandler(event) {
+        // @ts-expect-error
         if (window.CURRENT_TOOLTIP_ELEMENT &&
+            // @ts-expect-error
             !window.CURRENT_TOOLTIP_ELEMENT.contains(document.activeElement) &&
+            // @ts-expect-error
             !window.CURRENT_TOOLTIP_ELEMENT.contains(event.relatedTarget) &&
+            // @ts-expect-error
             !window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.contains(document.activeElement) &&
+            // @ts-expect-error
             !window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.contains(event.relatedTarget)
         ) {
             // Work around a difference in the focus behaviour between Firefox, Chrome, and Safari.
@@ -1251,32 +1402,45 @@ function preLoadCss(cssUrl) {
      *                          If set to `false`, leave keyboard focus alone.
      */
     function hideTooltip(focus) {
+        // @ts-expect-error
         if (window.CURRENT_TOOLTIP_ELEMENT) {
+            // @ts-expect-error
             if (window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.TOOLTIP_FORCE_VISIBLE) {
                 if (focus) {
+                    // @ts-expect-error
                     window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.focus();
                 }
+                // @ts-expect-error
                 window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.TOOLTIP_FORCE_VISIBLE = false;
             }
+            // @ts-expect-error
             document.body.removeChild(window.CURRENT_TOOLTIP_ELEMENT);
+            // @ts-expect-error
             clearTooltipHoverTimeout(window.CURRENT_TOOLTIP_ELEMENT);
+            // @ts-expect-error
             window.CURRENT_TOOLTIP_ELEMENT = null;
         }
     }
 
+    // @ts-expect-error
     onEachLazy(document.getElementsByClassName("tooltip"), e => {
         e.onclick = () => {
             e.TOOLTIP_FORCE_VISIBLE = e.TOOLTIP_FORCE_VISIBLE ? false : true;
+            // @ts-expect-error
             if (window.CURRENT_TOOLTIP_ELEMENT && !e.TOOLTIP_FORCE_VISIBLE) {
                 hideTooltip(true);
             } else {
                 showTooltip(e);
+                // @ts-expect-error
                 window.CURRENT_TOOLTIP_ELEMENT.setAttribute("tabindex", "0");
+                // @ts-expect-error
                 window.CURRENT_TOOLTIP_ELEMENT.focus();
+                // @ts-expect-error
                 window.CURRENT_TOOLTIP_ELEMENT.onblur = tooltipBlurHandler;
             }
             return false;
         };
+        // @ts-expect-error
         e.onpointerenter = ev => {
             // If this is a synthetic touch event, ignore it. A click event will be along shortly.
             if (ev.pointerType !== "mouse") {
@@ -1284,6 +1448,7 @@ function preLoadCss(cssUrl) {
             }
             setTooltipHoverTimeout(e, true);
         };
+        // @ts-expect-error
         e.onpointermove = ev => {
             // If this is a synthetic touch event, ignore it. A click event will be along shortly.
             if (ev.pointerType !== "mouse") {
@@ -1291,12 +1456,15 @@ function preLoadCss(cssUrl) {
             }
             setTooltipHoverTimeout(e, true);
         };
+        // @ts-expect-error
         e.onpointerleave = ev => {
             // If this is a synthetic touch event, ignore it. A click event will be along shortly.
             if (ev.pointerType !== "mouse") {
                 return;
             }
+            // @ts-expect-error
             if (!e.TOOLTIP_FORCE_VISIBLE && window.CURRENT_TOOLTIP_ELEMENT &&
+                // @ts-expect-error
                 !window.CURRENT_TOOLTIP_ELEMENT.contains(ev.relatedTarget)) {
                 // Tooltip pointer leave gesture:
                 //
@@ -1329,6 +1497,7 @@ function preLoadCss(cssUrl) {
                 // * https://www.nngroup.com/articles/tooltip-guidelines/
                 // * https://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown
                 setTooltipHoverTimeout(e, false);
+                // @ts-expect-error
                 addClass(window.CURRENT_TOOLTIP_ELEMENT, "fade-out");
             }
         };
@@ -1338,6 +1507,7 @@ function preLoadCss(cssUrl) {
     if (sidebar_menu_toggle) {
         sidebar_menu_toggle.addEventListener("click", () => {
             const sidebar = document.getElementsByClassName("sidebar")[0];
+            // @ts-expect-error
             if (!hasClass(sidebar, "shown")) {
                 showSidebar();
             } else {
@@ -1346,12 +1516,18 @@ function preLoadCss(cssUrl) {
         });
     }
 
+    // @ts-expect-error
     function helpBlurHandler(event) {
+        // @ts-expect-error
         if (!getHelpButton().contains(document.activeElement) &&
+            // @ts-expect-error
             !getHelpButton().contains(event.relatedTarget) &&
+            // @ts-expect-error
             !getSettingsButton().contains(document.activeElement) &&
+            // @ts-expect-error
             !getSettingsButton().contains(event.relatedTarget)
         ) {
+            // @ts-expect-error
             window.hidePopoverMenus();
         }
     }
@@ -1427,14 +1603,18 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
         if (isHelpPage) {
             const help_section = document.createElement("section");
             help_section.appendChild(container);
+            // @ts-expect-error
             document.getElementById("main-content").appendChild(help_section);
             container.style.display = "block";
         } else {
             const help_button = getHelpButton();
+            // @ts-expect-error
             help_button.appendChild(container);
 
             container.onblur = helpBlurHandler;
+            // @ts-expect-error
             help_button.onblur = helpBlurHandler;
+            // @ts-expect-error
             help_button.children[0].onblur = helpBlurHandler;
         }
 
@@ -1446,8 +1626,10 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
      *
      * Pass "true" to reset focus for tooltip popovers.
      */
+    // @ts-expect-error
     window.hideAllModals = switchFocus => {
         hideSidebar();
+        // @ts-expect-error
         window.hidePopoverMenus();
         hideTooltip(switchFocus);
     };
@@ -1455,7 +1637,9 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
     /**
      * Hide all the popover menus.
      */
+    // @ts-expect-error
     window.hidePopoverMenus = () => {
+        // @ts-expect-error
         onEachLazy(document.querySelectorAll("rustdoc-toolbar .popover"), elem => {
             elem.style.display = "none";
         });
@@ -1474,10 +1658,12 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
      * @return {HTMLElement}
      */
     function getHelpMenu(buildNeeded) {
+        // @ts-expect-error
         let menu = getHelpButton().querySelector(".popover");
         if (!menu && buildNeeded) {
             menu = buildHelpMenu();
         }
+        // @ts-expect-error
         return menu;
     }
 
@@ -1489,9 +1675,11 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
         // other modals.
         const button = getHelpButton();
         addClass(button, "help-open");
+        // @ts-expect-error
         button.querySelector("a").focus();
         const menu = getHelpMenu(true);
         if (menu.style.display === "none") {
+            // @ts-expect-error
             window.hideAllModals();
             menu.style.display = "";
         }
@@ -1506,8 +1694,11 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
             // If user clicks with a moderator, though, use default browser behavior,
             // probably opening in a new window or tab.
             if (!helpLink.contains(helpLink) ||
+                // @ts-expect-error
                 event.ctrlKey ||
+                // @ts-expect-error
                 event.altKey ||
+                // @ts-expect-error
                 event.metaKey) {
                 return;
             }
@@ -1517,6 +1708,7 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
             if (shouldShowHelp) {
                 showHelp();
             } else {
+                // @ts-expect-error
                 window.hidePopoverMenus();
             }
         });
@@ -1527,6 +1719,7 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
     addSidebarCrates();
     onHashChange(null);
     window.addEventListener("hashchange", onHashChange);
+    // @ts-expect-error
     searchState.setup();
 }());
 
@@ -1580,6 +1773,7 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
             removeClass(document.documentElement, "hide-sidebar");
             updateLocalStorage("hide-sidebar", "false");
             if (document.querySelector(".rustdoc.src")) {
+                // @ts-expect-error
                 window.rustdocToggleSrcSidebar();
             }
             e.preventDefault();
@@ -1589,6 +1783,7 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
     // Pointer capture.
     //
     // Resizing is a single-pointer gesture. Any secondary pointer is ignored
+    // @ts-expect-error
     let currentPointerId = null;
 
     // "Desired" sidebar size.
@@ -1596,6 +1791,7 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
     // This is stashed here for window resizing. If the sidebar gets
     // shrunk to maintain BODY_MIN, and then the user grows the window again,
     // it gets the sidebar to restore its size.
+    // @ts-expect-error
     let desiredSidebarSize = null;
 
     // Sidebar resize debouncer.
@@ -1626,7 +1822,9 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
     // through that size when using the shrink-to-nothing gesture.
     function hideSidebar() {
         if (isSrcPage) {
+            // @ts-expect-error
             window.rustdocCloseSourceSidebar();
+            // @ts-expect-error
             updateLocalStorage("src-sidebar-width", null);
             // [RUSTDOCIMPL] CSS variable fast path
             //
@@ -1639,14 +1837,19 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
             //
             // So, to clear it, we need to clear all three.
             document.documentElement.style.removeProperty("--src-sidebar-width");
+            // @ts-expect-error
             sidebar.style.removeProperty("--src-sidebar-width");
+            // @ts-expect-error
             resizer.style.removeProperty("--src-sidebar-width");
         } else {
             addClass(document.documentElement, "hide-sidebar");
             updateLocalStorage("hide-sidebar", "true");
+            // @ts-expect-error
             updateLocalStorage("desktop-sidebar-width", null);
             document.documentElement.style.removeProperty("--desktop-sidebar-width");
+            // @ts-expect-error
             sidebar.style.removeProperty("--desktop-sidebar-width");
+            // @ts-expect-error
             resizer.style.removeProperty("--desktop-sidebar-width");
         }
     }
@@ -1659,6 +1862,7 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
     // remains visible all the time on there.
     function showSidebar() {
         if (isSrcPage) {
+            // @ts-expect-error
             window.rustdocShowSourceSidebar();
         } else {
             removeClass(document.documentElement, "hide-sidebar");
@@ -1674,6 +1878,7 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
      */
     function changeSidebarSize(size) {
         if (isSrcPage) {
+            // @ts-expect-error
             updateLocalStorage("src-sidebar-width", size);
             // [RUSTDOCIMPL] CSS variable fast path
             //
@@ -1681,11 +1886,16 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
             // because the sidebar isn't actually loaded yet,
             // we scope this update to the sidebar to avoid hitting a slow
             // path in WebKit.
+            // @ts-expect-error
             sidebar.style.setProperty("--src-sidebar-width", size + "px");
+            // @ts-expect-error
             resizer.style.setProperty("--src-sidebar-width", size + "px");
         } else {
+            // @ts-expect-error
             updateLocalStorage("desktop-sidebar-width", size);
+            // @ts-expect-error
             sidebar.style.setProperty("--desktop-sidebar-width", size + "px");
+            // @ts-expect-error
             resizer.style.setProperty("--desktop-sidebar-width", size + "px");
         }
     }
@@ -1701,7 +1911,9 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
     // Respond to the resize handle event.
     // This function enforces size constraints, and implements the
     // shrink-to-nothing gesture based on thresholds defined above.
+    // @ts-expect-error
     function resize(e) {
+        // @ts-expect-error
         if (currentPointerId === null || currentPointerId !== e.pointerId) {
             return;
         }
@@ -1719,15 +1931,19 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
             changeSidebarSize(constrainedPos);
             desiredSidebarSize = constrainedPos;
             if (pendingSidebarResizingFrame !== false) {
+                // @ts-expect-error
                 clearTimeout(pendingSidebarResizingFrame);
             }
+            // @ts-expect-error
             pendingSidebarResizingFrame = setTimeout(() => {
+                // @ts-expect-error
                 if (currentPointerId === null || pendingSidebarResizingFrame === false) {
                     return;
                 }
                 pendingSidebarResizingFrame = false;
                 document.documentElement.style.setProperty(
                     "--resizing-sidebar-width",
+                    // @ts-expect-error
                     desiredSidebarSize + "px",
                 );
             }, 100);
@@ -1739,51 +1955,69 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
             return;
         }
         stopResize();
+        // @ts-expect-error
         if (desiredSidebarSize >= (window.innerWidth - BODY_MIN)) {
             changeSidebarSize(window.innerWidth - BODY_MIN);
+        // @ts-expect-error
         } else if (desiredSidebarSize !== null && desiredSidebarSize > SIDEBAR_MIN) {
+            // @ts-expect-error
             changeSidebarSize(desiredSidebarSize);
         }
     });
+    // @ts-expect-error
     function stopResize(e) {
+        // @ts-expect-error
         if (currentPointerId === null) {
             return;
         }
         if (e) {
             e.preventDefault();
         }
+        // @ts-expect-error
         desiredSidebarSize = sidebar.getBoundingClientRect().width;
+        // @ts-expect-error
         removeClass(resizer, "active");
         window.removeEventListener("pointermove", resize, false);
         window.removeEventListener("pointerup", stopResize, false);
         removeClass(document.documentElement, "sidebar-resizing");
         document.documentElement.style.removeProperty( "--resizing-sidebar-width");
+        // @ts-expect-error
         if (resizer.releasePointerCapture) {
+            // @ts-expect-error
             resizer.releasePointerCapture(currentPointerId);
             currentPointerId = null;
         }
     }
+    // @ts-expect-error
     function initResize(e) {
+        // @ts-expect-error
         if (currentPointerId !== null || e.altKey || e.ctrlKey || e.metaKey || e.button !== 0) {
             return;
         }
+        // @ts-expect-error
         if (resizer.setPointerCapture) {
+            // @ts-expect-error
             resizer.setPointerCapture(e.pointerId);
+            // @ts-expect-error
             if (!resizer.hasPointerCapture(e.pointerId)) {
                 // unable to capture pointer; something else has it
                 // on iOS, this usually means you long-clicked a link instead
+                // @ts-expect-error
                 resizer.releasePointerCapture(e.pointerId);
                 return;
             }
             currentPointerId = e.pointerId;
         }
+        // @ts-expect-error
         window.hideAllModals(false);
         e.preventDefault();
         window.addEventListener("pointermove", resize, false);
         window.addEventListener("pointercancel", stopResize, false);
         window.addEventListener("pointerup", stopResize, false);
+        // @ts-expect-error
         addClass(resizer, "active");
         addClass(document.documentElement, "sidebar-resizing");
+        // @ts-expect-error
         const pos = e.clientX - sidebar.offsetLeft - 3;
         document.documentElement.style.setProperty( "--resizing-sidebar-width", pos + "px");
         desiredSidebarSize = null;
@@ -1795,6 +2029,7 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
 // and the copy buttons on the code examples.
 (function() {
     // Common functions to copy buttons.
+    // @ts-expect-error
     function copyContentToClipboard(content) {
         const el = document.createElement("textarea");
         el.value = content;
@@ -1809,6 +2044,7 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
         document.body.removeChild(el);
     }
 
+    // @ts-expect-error
     function copyButtonAnimation(button) {
         button.classList.add("clicked");
 
@@ -1831,6 +2067,7 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
         // Most page titles are '<Item> in <path::to::module> - Rust', except
         // modules (which don't have the first part) and keywords/primitives
         // (which don't have a module path)
+        // @ts-expect-error
         const title = document.querySelector("title").textContent.replace(" - Rust", "");
         const [item, module] = title.split(" in ");
         const path = [item];
@@ -1843,6 +2080,7 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
     };
 
     // Copy buttons on code examples.
+    // @ts-expect-error
     function copyCode(codeElem) {
         if (!codeElem) {
             // Should never happen, but the world is a dark and dangerous place.
@@ -1851,6 +2089,7 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
         copyContentToClipboard(codeElem.textContent);
     }
 
+    // @ts-expect-error
     function getExampleWrap(event) {
         let elem = event.target;
         while (!hasClass(elem, "example-wrap")) {
@@ -1866,6 +2105,7 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
         return elem;
     }
 
+    // @ts-expect-error
     function addCopyButton(event) {
         const elem = getExampleWrap(event);
         if (elem === null) {
@@ -1896,9 +2136,11 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
             return;
         }
         const scrapedWrapped = elem.parentElement;
+        // @ts-expect-error
         window.updateScrapedExample(scrapedWrapped, parent);
     }
 
+    // @ts-expect-error
     function showHideCodeExampleButtons(event) {
         const elem = getExampleWrap(event);
         if (elem === null) {
@@ -1917,6 +2159,7 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
         buttons.classList.toggle("keep-visible");
     }
 
+    // @ts-expect-error
     onEachLazy(document.querySelectorAll(".docblock .example-wrap"), elem => {
         elem.addEventListener("mouseover", addCopyButton);
         elem.addEventListener("click", showHideCodeExampleButtons);
diff --git a/src/librustdoc/html/static/js/rustdoc.d.ts b/src/librustdoc/html/static/js/rustdoc.d.ts
new file mode 100644
index 0000000000000..18a3e22113b8b
--- /dev/null
+++ b/src/librustdoc/html/static/js/rustdoc.d.ts
@@ -0,0 +1,387 @@
+// This file contains type definitions that are processed by the TypeScript Compiler but are
+// not put into the JavaScript we include as part of the documentation. It is used for
+// type checking. See README.md in this directory for more info.
+
+/* eslint-disable */
+declare global {
+    interface Window {
+        /** Make the current theme easy to find */
+        currentTheme: HTMLLinkElement|null;
+        /** Used by the popover tooltip code. */
+        RUSTDOC_TOOLTIP_HOVER_MS: number;
+        /** Used by the popover tooltip code. */
+        RUSTDOC_TOOLTIP_HOVER_EXIT_MS: number;
+        /** Search engine data used by main.js and search.js */
+        searchState: rustdoc.SearchState;
+        /** Global option, with a long list of "../"'s */
+        rootPath: string|null;
+        /**
+         * Currently opened crate.
+         * As a multi-page application, we know this never changes once set.
+         */
+        currentCrate: string|null;
+    }
+    interface HTMLElement {
+        /** Used by the popover tooltip code. */
+        TOOLTIP_FORCE_VISIBLE: boolean|undefined,
+        /** Used by the popover tooltip code */
+        TOOLTIP_HOVER_TIMEOUT: Timeout|undefined,
+    }
+}
+
+export = rustdoc;
+
+declare namespace rustdoc {
+    interface SearchState {
+        rustdocToolbar: HTMLElement|null;
+        loadingText: string;
+        input: HTMLInputElement|null;
+        title: string;
+        titleBeforeSearch: string;
+        timeout: number|null;
+        currentTab: number;
+        focusedByTab: [number|null, number|null, number|null];
+        clearInputTimeout: function;
+        outputElement: function(): HTMLElement|null;
+        focus: function();
+        defocus: function();
+        showResults: function(HTMLElement|null|undefined);
+        removeQueryParameters: function();
+        hideResults: function();
+        getQueryStringParams: function(): Object.<any, string>;
+        origPlaceholder: string;
+        setup: function();
+        setLoadingSearch: function();
+        descShards: Map<string, SearchDescShard[]>;
+        loadDesc: function({descShard: SearchDescShard, descIndex: number}): Promise<string|null>;
+        loadedDescShard: function(string, number, string);
+        isDisplayed: function(): boolean,
+    }
+
+    interface SearchDescShard {
+        crate: string;
+        promise: Promise<string[]>|null;
+        resolve: function(string[])|null;
+        shard: number;
+    }
+
+    /**
+     * A single parsed "atom" in a search query. For example,
+     * 
+     *     std::fmt::Formatter, Write -> Result<()>
+     *     ┏━━━━━━━━━━━━━━━━━━  ┌────    ┏━━━━━┅┅┅┅┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
+     *     ┃                    │        ┗ QueryElement {        ┊
+     *     ┃                    │              name: Result      ┊
+     *     ┃                    │              generics: [       ┊
+     *     ┃                    │                   QueryElement ┘
+     *     ┃                    │                   name: ()
+     *     ┃                    │              ]
+     *     ┃                    │          }
+     *     ┃                    └ QueryElement {
+     *     ┃                          name: Write
+     *     ┃                      }
+     *     ┗ QueryElement {
+     *           name: Formatter
+     *           pathWithoutLast: std::fmt
+     *       }
+     */
+    interface QueryElement {
+        name: string,
+        id: number|null,
+        fullPath: Array<string>,
+        pathWithoutLast: Array<string>,
+        pathLast: string,
+        normalizedPathLast: string,
+        generics: Array<QueryElement>,
+        bindings: Map<number, Array<QueryElement>>,
+        typeFilter: number|null,
+    }
+
+    /**
+     * Same as QueryElement, but bindings and typeFilter support strings
+     */
+    interface ParserQueryElement {
+        name: string,
+        id: number|null,
+        fullPath: Array<string>,
+        pathWithoutLast: Array<string>,
+        pathLast: string,
+        normalizedPathLast: string,
+        generics: Array<ParserQueryElement>,
+        bindings: Map<string, Array<ParserQueryElement>>,
+        bindingName: {name: string, generics: ParserQueryElement[]}|null,
+        typeFilter: string|null,
+    }
+
+    /**
+     * Intermediate parser state. Discarded when parsing is done.
+     */
+    interface ParserState {
+        pos: number;
+        length: number;
+        totalElems: number;
+        genericsElems: number;
+        typeFilter: (null|string);
+        userQuery: string;
+        isInBinding: (null|{name: string, generics: ParserQueryElement[]});
+    }
+
+    /**
+     * A complete parsed query.
+     */
+    interface ParsedQuery<T> {
+        userQuery: string,
+        elems: Array<T>,
+        returned: Array<T>,
+        foundElems: number,
+        totalElems: number,
+        literalSearch: boolean,
+        hasReturnArrow: boolean,
+        correction: string|null,
+        proposeCorrectionFrom: string|null,
+        proposeCorrectionTo: string|null,
+        typeFingerprint: Uint32Array,
+        error: Array<string> | null,
+    }
+
+    /**
+     * An entry in the search index database.
+     */
+    interface Row {
+        crate: string,
+        descShard: SearchDescShard,
+        id: number,
+        name: string,
+        normalizedName: string,
+        word: string,
+        parent: ({ty: number, name: string, path: string, exactPath: string}|null|undefined),
+        path: string,
+        ty: number,
+        type?: FunctionSearchType
+    }
+
+    /**
+     * The viewmodel for the search engine results page.
+     */
+    interface ResultsTable {
+        in_args: Array<ResultObject>,
+        returned: Array<ResultObject>,
+        others: Array<ResultObject>,
+        query: ParsedQuery,
+    }
+
+    type Results = Map<String, ResultObject>;
+
+    /**
+     * An annotated `Row`, used in the viewmodel.
+     */
+    interface ResultObject {
+        desc: string,
+        displayPath: string,
+        fullPath: string,
+        href: string,
+        id: number,
+        dist: number,
+        path_dist: number,
+        name: string,
+        normalizedName: string,
+        word: string,
+        index: number,
+        parent: (Object|undefined),
+        path: string,
+        ty: number,
+        type?: FunctionSearchType,
+        paramNames?: string[],
+        displayType: Promise<Array<Array<string>>>|null,
+        displayTypeMappedNames: Promise<Array<[string, Array<string>]>>|null,
+        item: Row,
+        dontValidate?: boolean,
+    }
+
+    /**
+     * A pair of [inputs, outputs], or 0 for null. This is stored in the search index.
+     * The JavaScript deserializes this into FunctionSearchType.
+     *
+     * Numeric IDs are *ONE-indexed* into the paths array (`p`). Zero is used as a sentinel for `null`
+     * because `null` is four bytes while `0` is one byte.
+     *
+     * An input or output can be encoded as just a number if there is only one of them, AND
+     * it has no generics. The no generics rule exists to avoid ambiguity: imagine if you had
+     * a function with a single output, and that output had a single generic:
+     *
+     *     fn something() -> Result<usize, usize>
+     *
+     * If output was allowed to be any RawFunctionType, it would look like thi
+     *
+     *     [[], [50, [3, 3]]]
+     *
+     * The problem is that the above output could be interpreted as either a type with ID 50 and two
+     * generics, or it could be interpreted as a pair of types, the first one with ID 50 and the second
+     * with ID 3 and a single generic parameter that is also ID 3. We avoid this ambiguity by choosing
+     * in favor of the pair of types interpretation. This is why the `(number|Array<RawFunctionType>)`
+     * is used instead of `(RawFunctionType|Array<RawFunctionType>)`.
+     *
+     * The output can be skipped if it's actually unit and there's no type constraints. If thi
+     * function accepts constrained generics, then the output will be unconditionally emitted, and
+     * after it will come a list of trait constraints. The position of the item in the list will
+     * determine which type parameter it is. For example:
+     *
+     *     [1, 2, 3, 4, 5]
+     *      ^  ^  ^  ^  ^
+     *      |  |  |  |  - generic parameter (-3) of trait 5
+     *      |  |  |  - generic parameter (-2) of trait 4
+     *      |  |  - generic parameter (-1) of trait 3
+     *      |  - this function returns a single value (type 2)
+     *      - this function takes a single input parameter (type 1)
+     *
+     * Or, for a less contrived version:
+     *
+     *     [[[4, -1], 3], [[5, -1]], 11]
+     *      -^^^^^^^----   ^^^^^^^   ^^
+     *       |        |    |          - generic parameter, roughly `where -1: 11`
+     *       |        |    |            since -1 is the type parameter and 11 the trait
+     *       |        |    - function output 5<-1>
+     *       |        - the overall function signature is something like
+     *       |          `fn(4<-1>, 3) -> 5<-1> where -1: 11`
+     *       - function input, corresponds roughly to 4<-1>
+     *         4 is an index into the `p` array for a type
+     *         -1 is the generic parameter, given by 11
+     *
+     * If a generic parameter has multiple trait constraints, it gets wrapped in an array, just like
+     * function inputs and outputs:
+     *
+     *     [-1, -1, [4, 3]]
+     *              ^^^^^^ where -1: 4 + 3
+     *
+     * If a generic parameter's trait constraint has generic parameters, it gets wrapped in the array
+     * even if only one exists. In other words, the ambiguity of `4<3>` and `4 + 3` is resolved in
+     * favor of `4 + 3`:
+     *
+     *     [-1, -1, [[4, 3]]]
+     *              ^^^^^^^^ where -1: 4 + 3
+     *
+     *     [-1, -1, [5, [4, 3]]]
+     *              ^^^^^^^^^^^ where -1: 5, -2: 4 + 3
+     *
+     * If a generic parameter has no trait constraints (like in Rust, the `Sized` constraint i
+     * implied and a fake `?Sized` constraint used to note its absence), it will be filled in with 0.
+     */
+    type RawFunctionSearchType =
+        0 |
+        [(number|Array<RawFunctionType>)] |
+        [(number|Array<RawFunctionType>), (number|Array<RawFunctionType>)] |
+        Array<(number|Array<RawFunctionType>)>
+    ;
+
+    /**
+     * A single function input or output type. This is either a single path ID, or a pair of
+     * [path ID, generics].
+     *
+     * Numeric IDs are *ONE-indexed* into the paths array (`p`). Zero is used as a sentinel for `null`
+     * because `null` is four bytes while `0` is one byte.
+     */
+    type RawFunctionType = number | [number, Array<RawFunctionType>];
+
+    /**
+     * The type signature entry in the decoded search index.
+     * (The "Raw" objects are encoded differently to save space in the JSON).
+     */
+    interface FunctionSearchType {
+        inputs: Array<FunctionType>,
+        output: Array<FunctionType>,
+        where_clause: Array<Array<FunctionType>>,
+    }
+
+    /**
+     * A decoded function type, made from real objects.
+     * `ty` will be negative for generics, positive for types, and 0 for placeholders.
+     */
+    interface FunctionType {
+        id: null|number,
+        ty: number|null,
+        name?: string,
+        path: string|null,
+        exactPath: string|null,
+        unboxFlag: boolean,
+        generics: Array<FunctionType>,
+        bindings: Map<number, Array<FunctionType>>,
+    };
+
+    interface HighlightedFunctionType extends FunctionType {
+        generics: HighlightedFunctionType[],
+        bindings: Map<number, HighlightedFunctionType[]>,
+        highlighted?: boolean;
+    }
+
+    interface FingerprintableType {
+        id: number|null;
+        generics: FingerprintableType[];
+        bindings: Map<number, FingerprintableType[]>;
+    };
+
+    /**
+     * The raw search data for a given crate. `n`, `t`, `d`, `i`, and `f`
+     * are arrays with the same length. `q`, `a`, and `c` use a sparse
+     * representation for compactness.
+     *
+     * `n[i]` contains the name of an item.
+     *
+     * `t[i]` contains the type of that item
+     * (as a string of characters that represent an offset in `itemTypes`).
+     *
+     * `d[i]` contains the description of that item.
+     *
+     * `q` contains the full paths of the items. For compactness, it is a set of
+     * (index, path) pairs used to create a map. If a given index `i` is
+     * not present, this indicates "same as the last index present".
+     *
+     * `i[i]` contains an item's parent, usually a module. For compactness,
+     * it is a set of indexes into the `p` array.
+     *
+     * `f` contains function signatures, or `0` if the item isn't a function.
+     * More information on how they're encoded can be found in rustc-dev-guide
+     *
+     * Functions are themselves encoded as arrays. The first item is a list of
+     * types representing the function's inputs, and the second list item is a list
+     * of types representing the function's output. Tuples are flattened.
+     * Types are also represented as arrays; the first item is an index into the `p`
+     * array, while the second is a list of types representing any generic parameters.
+     *
+     * b[i] contains an item's impl disambiguator. This is only present if an item
+     * is defined in an impl block and, the impl block's type has more than one associated
+     * item with the same name.
+     *
+     * `a` defines aliases with an Array of pairs: [name, offset], where `offset`
+     * points into the n/t/d/q/i/f arrays.
+     *
+     * `doc` contains the description of the crate.
+     *
+     * `p` is a list of path/type pairs. It is used for parents and function parameters.
+     * The first item is the type, the second is the name, the third is the visible path (if any) and
+     * the fourth is the canonical path used for deduplication (if any).
+     *
+     * `r` is the canonical path used for deduplication of re-exported items.
+     * It is not used for associated items like methods (that's the fourth element
+     * of `p`) but is used for modules items like free functions.
+     *
+     * `c` is an array of item indices that are deprecated.
+     */
+    type RawSearchIndexCrate = {
+    doc: string,
+    a: Object,
+    n: Array<string>,
+    t: string,
+    D: string,
+    e: string,
+    q: Array<[number, string]>,
+    i: string,
+    f: string,
+    p: Array<[number, string] | [number, string, number] | [number, string, number, number] | [number, string, number, number, string]>,
+    b: Array<[number, String]>,
+    c: string,
+    r: Array<[number, number]>,
+    P: Array<[number, string]>,
+    };
+
+    type VlqData = VlqData[] | number;
+}
diff --git a/src/librustdoc/html/static/js/scrape-examples.js b/src/librustdoc/html/static/js/scrape-examples.js
index 98c53b8656f52..d08f15a5bfa86 100644
--- a/src/librustdoc/html/static/js/scrape-examples.js
+++ b/src/librustdoc/html/static/js/scrape-examples.js
@@ -1,5 +1,8 @@
 /* global addClass, hasClass, removeClass, onEachLazy */
 
+// Eventually fix this.
+// @ts-nocheck
+
 "use strict";
 
 (function() {
diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js
index 660484c133c36..1ad32721e0687 100644
--- a/src/librustdoc/html/static/js/search.js
+++ b/src/librustdoc/html/static/js/search.js
@@ -10,11 +10,19 @@ if (!Array.prototype.toSpliced) {
     // Can't use arrow functions, because we want `this`
     Array.prototype.toSpliced = function() {
         const me = this.slice();
+        // @ts-expect-error
         Array.prototype.splice.apply(me, arguments);
         return me;
     };
 }
 
+/**
+ *
+ * @template T
+ * @param {Iterable<T>} arr
+ * @param {function(T): any} func
+ * @param {function(T): boolean} funcBtwn
+ */
 function onEachBtwn(arr, func, funcBtwn) {
     let skipped = true;
     for (const value of arr) {
@@ -98,9 +106,24 @@ const NO_TYPE_FILTER = -1;
  * documentation.
  */
 const editDistanceState = {
+    /**
+     * @type {number[]}
+     */
     current: [],
+    /**
+     * @type {number[]}
+     */
     prev: [],
+    /**
+     * @type {number[]}
+     */
     prevPrev: [],
+    /**
+     * @param {string} a
+     * @param {string} b
+     * @param {number} limit
+     * @returns
+     */
     calculate: function calculate(a, b, limit) {
         // Ensure that `b` is the shorter string, minimizing memory use.
         if (a.length < b.length) {
@@ -186,14 +209,28 @@ const editDistanceState = {
     },
 };
 
+/**
+ * @param {string} a
+ * @param {string} b
+ * @param {number} limit
+ * @returns
+ */
 function editDistance(a, b, limit) {
     return editDistanceState.calculate(a, b, limit);
 }
 
+/**
+ * @param {string} c
+ * @returns {boolean}
+ */
 function isEndCharacter(c) {
     return "=,>-])".indexOf(c) !== -1;
 }
 
+/**
+ * @param {number} ty
+ * @returns
+ */
 function isFnLikeTy(ty) {
     return ty === TY_FN || ty === TY_METHOD || ty === TY_TYMETHOD;
 }
@@ -212,7 +249,7 @@ function isSeparatorCharacter(c) {
 /**
  * Returns `true` if the current parser position is starting with "->".
  *
- * @param {ParserState} parserState
+ * @param {rustdoc.ParserState} parserState
  *
  * @return {boolean}
  */
@@ -223,7 +260,7 @@ function isReturnArrow(parserState) {
 /**
  * Increase current parser position until it doesn't find a whitespace anymore.
  *
- * @param {ParserState} parserState
+ * @param {rustdoc.ParserState} parserState
  */
 function skipWhitespace(parserState) {
     while (parserState.pos < parserState.userQuery.length) {
@@ -238,7 +275,7 @@ function skipWhitespace(parserState) {
 /**
  * Returns `true` if the previous character is `lookingFor`.
  *
- * @param {ParserState} parserState
+ * @param {rustdoc.ParserState} parserState
  * @param {String} lookingFor
  *
  * @return {boolean}
@@ -260,8 +297,8 @@ function prevIs(parserState, lookingFor) {
 /**
  * Returns `true` if the last element in the `elems` argument has generics.
  *
- * @param {Array<QueryElement>} elems
- * @param {ParserState} parserState
+ * @param {Array<rustdoc.ParserQueryElement>} elems
+ * @param {rustdoc.ParserState} parserState
  *
  * @return {boolean}
  */
@@ -270,6 +307,13 @@ function isLastElemGeneric(elems, parserState) {
         prevIs(parserState, ">");
 }
 
+/**
+ *
+ * @param {rustdoc.ParsedQuery<rustdoc.ParserQueryElement>} query
+ * @param {rustdoc.ParserState} parserState
+ * @param {rustdoc.ParserQueryElement[]} elems
+ * @param {boolean} isInGenerics
+ */
 function getFilteredNextElem(query, parserState, elems, isInGenerics) {
     const start = parserState.pos;
     if (parserState.userQuery[parserState.pos] === ":" && !isPathStart(parserState)) {
@@ -294,6 +338,8 @@ function getFilteredNextElem(query, parserState, elems, isInGenerics) {
         // The type filter doesn't count as an element since it's a modifier.
         const typeFilterElem = elems.pop();
         checkExtraTypeFilterCharacters(start, parserState);
+        // typeFilterElem is not null. If it was, the elems.length check would have fired.
+        // @ts-expect-error
         parserState.typeFilter = typeFilterElem.normalizedPathLast;
         parserState.pos += 1;
         parserState.totalElems -= 1;
@@ -309,12 +355,13 @@ function getFilteredNextElem(query, parserState, elems, isInGenerics) {
  * If there is no `endChar`, this function will implicitly stop at the end
  * without raising an error.
  *
- * @param {ParsedQuery} query
- * @param {ParserState} parserState
- * @param {Array<QueryElement>} elems - This is where the new {QueryElement} will be added.
- * @param {string} endChar            - This function will stop when it'll encounter this
- *                                      character.
- * @returns {{foundSeparator: bool}}
+ * @param {rustdoc.ParsedQuery<rustdoc.ParserQueryElement>} query
+ * @param {rustdoc.ParserState} parserState
+ * @param {Array<rustdoc.ParserQueryElement>} elems
+ *     - This is where the new {QueryElement} will be added.
+ * @param {string} endChar - This function will stop when it'll encounter this
+ *                           character.
+ * @returns {{foundSeparator: boolean}}
  */
 function getItemsBefore(query, parserState, elems, endChar) {
     let foundStopChar = true;
@@ -385,6 +432,7 @@ function getItemsBefore(query, parserState, elems, endChar) {
             throw ["Unexpected ", c, " after ", extra];
         }
         if (!foundStopChar) {
+            /** @type {string[]} */
             let extra = [];
             if (isLastElemGeneric(query.elems, parserState)) {
                 extra = [" after ", ">"];
@@ -463,12 +511,14 @@ function getItemsBefore(query, parserState, elems, endChar) {
 }
 
 /**
- * @param {ParsedQuery} query
- * @param {ParserState} parserState
- * @param {Array<QueryElement>} elems - This is where the new {QueryElement} will be added.
+ * @param {rustdoc.ParsedQuery<rustdoc.ParserQueryElement>} query
+ * @param {rustdoc.ParserState} parserState
+ * @param {Array<rustdoc.ParserQueryElement>} elems
+ *     - This is where the new {QueryElement} will be added.
  * @param {boolean} isInGenerics
  */
 function getNextElem(query, parserState, elems, isInGenerics) {
+    /** @type {rustdoc.ParserQueryElement[]} */
     const generics = [];
 
     skipWhitespace(parserState);
@@ -588,6 +638,7 @@ function getNextElem(query, parserState, elems, isInGenerics) {
                 getFilteredNextElem(query, parserState, generics, isInGenerics);
                 generics[generics.length - 1].bindingName = makePrimitiveElement("output");
             } else {
+                // @ts-expect-error
                 generics.push(makePrimitiveElement(null, {
                     bindingName: makePrimitiveElement("output"),
                     typeFilter: null,
@@ -640,7 +691,8 @@ function getNextElem(query, parserState, elems, isInGenerics) {
  * Checks that the type filter doesn't have unwanted characters like `<>` (which are ignored
  * if empty).
  *
- * @param {ParserState} parserState
+ * @param {number} start
+ * @param {rustdoc.ParserState} parserState
  */
 function checkExtraTypeFilterCharacters(start, parserState) {
     const query = parserState.userQuery.slice(start, parserState.pos).trim();
@@ -658,12 +710,13 @@ function checkExtraTypeFilterCharacters(start, parserState) {
 }
 
 /**
- * @param {ParsedQuery} query
- * @param {ParserState} parserState
- * @param {string} name                  - Name of the query element.
- * @param {Array<QueryElement>} generics - List of generics of this query element.
+ * @param {rustdoc.ParsedQuery<rustdoc.ParserQueryElement>} query
+ * @param {rustdoc.ParserState} parserState
+ * @param {string} name - Name of the query element.
+ * @param {Array<rustdoc.ParserQueryElement>} generics - List of generics of this query element.
+ * @param {boolean} isInGenerics
  *
- * @return {QueryElement}                - The newly created `QueryElement`.
+ * @return {rustdoc.ParserQueryElement} - The newly created `QueryElement`.
  */
 function createQueryElement(query, parserState, name, generics, isInGenerics) {
     const path = name.trim();
@@ -756,9 +809,15 @@ function createQueryElement(query, parserState, name, generics, isInGenerics) {
     };
 }
 
+/**
+ *
+ * @param {string} name
+ * @param {Object=} extra
+ * @returns {rustdoc.ParserQueryElement}
+ */
 function makePrimitiveElement(name, extra) {
     return Object.assign({
-        name,
+        name: name,
         id: null,
         fullPath: [name],
         pathWithoutLast: [],
@@ -781,8 +840,8 @@ function makePrimitiveElement(name, extra) {
  * * There is more than one element.
  * * There is no closing `"`.
  *
- * @param {ParsedQuery} query
- * @param {ParserState} parserState
+ * @param {rustdoc.ParsedQuery<rustdoc.ParserQueryElement>} query
+ * @param {rustdoc.ParserState} parserState
  * @param {boolean} isInGenerics
  */
 function getStringElem(query, parserState, isInGenerics) {
@@ -813,9 +872,9 @@ function getStringElem(query, parserState, isInGenerics) {
  * character or the end of the query. It returns the position of the last
  * character of the ident.
  *
- * @param {ParserState} parserState
+ * @param {rustdoc.ParserState} parserState
  *
- * @return {integer}
+ * @return {number}
  */
 function getIdentEndPosition(parserState) {
     let afterIdent = consumeIdent(parserState);
@@ -890,6 +949,10 @@ function getIdentEndPosition(parserState) {
     return end;
 }
 
+/**
+ * @param {string} c
+ * @returns
+ */
 function isSpecialStartCharacter(c) {
     return "<\"".indexOf(c) !== -1;
 }
@@ -897,7 +960,7 @@ function isSpecialStartCharacter(c) {
 /**
  * Returns `true` if the current parser position is starting with "::".
  *
- * @param {ParserState} parserState
+ * @param {rustdoc.ParserState} parserState
  *
  * @return {boolean}
  */
@@ -909,7 +972,7 @@ function isPathStart(parserState) {
  * If the current parser position is at the beginning of an identifier,
  * move the position to the end of it and return `true`. Otherwise, return `false`.
  *
- * @param {ParserState} parserState
+ * @param {rustdoc.ParserState} parserState
  *
  * @return {boolean}
  */
@@ -935,14 +998,25 @@ function isPathSeparator(c) {
     return c === ":" || c === " ";
 }
 
+/**
+ * @template T
+ */
 class VlqHexDecoder {
+    /**
+     * @param {string} string
+     * @param {function(rustdoc.VlqData): T} cons
+     */
     constructor(string, cons) {
         this.string = string;
         this.cons = cons;
         this.offset = 0;
+        /** @type {T[]} */
         this.backrefQueue = [];
     }
-    // call after consuming `{`
+    /**
+     * call after consuming `{`
+     * @returns {rustdoc.VlqData[]}
+     */
     decodeList() {
         let c = this.string.charCodeAt(this.offset);
         const ret = [];
@@ -953,7 +1027,10 @@ class VlqHexDecoder {
         this.offset += 1; // eat cb
         return ret;
     }
-    // consumes and returns a list or integer
+    /**
+     * consumes and returns a list or integer
+     * @returns {rustdoc.VlqData}
+     */
     decode() {
         let n = 0;
         let c = this.string.charCodeAt(this.offset);
@@ -972,6 +1049,9 @@ class VlqHexDecoder {
         this.offset += 1;
         return sign ? -value : value;
     }
+    /**
+     * @returns {T}
+     */
     next() {
         const c = this.string.charCodeAt(this.offset);
         // sixteen characters after "0" are backref
@@ -994,6 +1074,7 @@ class VlqHexDecoder {
     }
 }
 class RoaringBitmap {
+    /** @param {string} str */
     constructor(str) {
         // https://github.com/RoaringBitmap/RoaringFormatSpec
         //
@@ -1063,6 +1144,7 @@ class RoaringBitmap {
             }
         }
     }
+    /** @param {number} keyvalue */
     contains(keyvalue) {
         const key = keyvalue >> 16;
         const value = keyvalue & 0xFFFF;
@@ -1091,10 +1173,15 @@ class RoaringBitmap {
 }
 
 class RoaringBitmapRun {
+    /**
+     * @param {number} runcount
+     * @param {Uint8Array} array
+     */
     constructor(runcount, array) {
         this.runcount = runcount;
         this.array = array;
     }
+    /** @param {number} value */
     contains(value) {
         // Binary search algorithm copied from
         // https://en.wikipedia.org/wiki/Binary_search#Procedure
@@ -1120,10 +1207,15 @@ class RoaringBitmapRun {
     }
 }
 class RoaringBitmapArray {
+    /**
+     * @param {number} cardinality
+     * @param {Uint8Array} array
+     */
     constructor(cardinality, array) {
         this.cardinality = cardinality;
         this.array = array;
     }
+    /** @param {number} value */
     contains(value) {
         // Binary search algorithm copied from
         // https://en.wikipedia.org/wiki/Binary_search#Procedure
@@ -1148,9 +1240,13 @@ class RoaringBitmapArray {
     }
 }
 class RoaringBitmapBits {
+    /**
+     * @param {Uint8Array} array
+     */
     constructor(array) {
         this.array = array;
     }
+    /** @param {number} value */
     contains(value) {
         return !!(this.array[value >> 3] & (1 << (value & 7)));
     }
@@ -1176,9 +1272,9 @@ class RoaringBitmapBits {
  * matches
  * : A list of search index IDs for this node.
  *
- * @typedef {{
- *     children: [NameTrie],
- *     matches: [number],
+ * @type {{
+ *     children: NameTrie[],
+ *     matches: number[],
  * }}
  */
 class NameTrie {
@@ -1186,9 +1282,20 @@ class NameTrie {
         this.children = [];
         this.matches = [];
     }
+    /**
+     * @param {string} name
+     * @param {number} id
+     * @param {Map<string, NameTrie[]>} tailTable
+     */
     insert(name, id, tailTable) {
         this.insertSubstring(name, 0, id, tailTable);
     }
+    /**
+     * @param {string} name
+     * @param {number} substart
+     * @param {number} id
+     * @param {Map<string, NameTrie[]>} tailTable
+     */
     insertSubstring(name, substart, id, tailTable) {
         const l = name.length;
         if (substart === l) {
@@ -1201,10 +1308,13 @@ class NameTrie {
             } else {
                 child = new NameTrie();
                 this.children[sb] = child;
+                /** @type {NameTrie[]} */
                 let sste;
                 if (substart >= 2) {
                     const tail = name.substring(substart - 2, substart + 1);
                     if (tailTable.has(tail)) {
+                        // it's not undefined
+                        // @ts-expect-error
                         sste = tailTable.get(tail);
                     } else {
                         sste = [];
@@ -1216,6 +1326,10 @@ class NameTrie {
             child.insertSubstring(name, substart + 1, id, tailTable);
         }
     }
+    /**
+     * @param {string} name
+     * @param {Map<string, NameTrie[]>} tailTable
+     */
     search(name, tailTable) {
         const results = new Set();
         this.searchSubstringPrefix(name, 0, results);
@@ -1226,6 +1340,8 @@ class NameTrie {
             this.searchLev(name, 0, levParams, results);
             const tail = name.substring(0, 3);
             if (tailTable.has(tail)) {
+                // it's not undefined
+                // @ts-expect-error
                 for (const entry of tailTable.get(tail)) {
                     entry.searchSubstringPrefix(name, 3, results);
                 }
@@ -1233,6 +1349,11 @@ class NameTrie {
         }
         return [...results];
     }
+    /**
+     * @param {string} name
+     * @param {number} substart
+     * @param {Set<number>} results
+     */
     searchSubstringPrefix(name, substart, results) {
         const l = name.length;
         if (substart === l) {
@@ -1240,14 +1361,18 @@ class NameTrie {
                 results.add(match);
             }
             // breadth-first traversal orders prefix matches by length
+            /** @type {NameTrie[]} */
             let unprocessedChildren = [];
             for (const child of this.children) {
                 if (child) {
                     unprocessedChildren.push(child);
                 }
             }
+            /** @type {NameTrie[]} */
             let nextSet = [];
             while (unprocessedChildren.length !== 0) {
+                /** @type {NameTrie} */
+                // @ts-expect-error
                 const next = unprocessedChildren.pop();
                 for (const child of next.children) {
                     if (child) {
@@ -1270,10 +1395,18 @@ class NameTrie {
             }
         }
     }
+    /**
+     * @param {string} name
+     * @param {number} substart
+     * @param {Lev2TParametricDescription|Lev1TParametricDescription} levParams
+     * @param {Set<number>} results
+     */
     searchLev(name, substart, levParams, results) {
         const stack = [[this, 0]];
         const n = levParams.n;
         while (stack.length !== 0) {
+            // It's not empty
+            //@ts-expect-error
             const [trie, levState] = stack.pop();
             for (const [charCode, child] of trie.children.entries()) {
                 if (!child) {
@@ -1305,6 +1438,11 @@ class NameTrie {
 }
 
 class DocSearch {
+    /**
+     * @param {Map<string, rustdoc.RawSearchIndexCrate>} rawSearchIndex
+     * @param {string} rootPath
+     * @param {rustdoc.SearchState} searchState
+     */
     constructor(rawSearchIndex, rootPath, searchState) {
         /**
          * @type {Map<String, RoaringBitmap>}
@@ -1317,19 +1455,19 @@ class DocSearch {
         /**
          *  @type {Uint32Array}
          */
-        this.functionTypeFingerprint = null;
+        this.functionTypeFingerprint = new Uint32Array(0);
         /**
          * Map from normalized type names to integers. Used to make type search
          * more efficient.
          *
-         * @type {Map<string, {id: integer, assocOnly: boolean}>}
+         * @type {Map<string, {id: number, assocOnly: boolean}>}
          */
         this.typeNameIdMap = new Map();
         /**
          * Map from type ID to associated type name. Used for display,
          * not for search.
          *
-         * @type {Map<integer, string>}
+         * @type {Map<number, string>}
          */
         this.assocTypeIdNameMap = new Map();
         this.ALIASES = new Map();
@@ -1338,64 +1476,88 @@ class DocSearch {
 
         /**
          * Special type name IDs for searching by array.
+         * @type {number}
          */
+        // @ts-expect-error
         this.typeNameIdOfArray = this.buildTypeMapIndex("array");
         /**
          * Special type name IDs for searching by slice.
+         * @type {number}
          */
+        // @ts-expect-error
         this.typeNameIdOfSlice = this.buildTypeMapIndex("slice");
         /**
          * Special type name IDs for searching by both array and slice (`[]` syntax).
+         * @type {number}
          */
+        // @ts-expect-error
         this.typeNameIdOfArrayOrSlice = this.buildTypeMapIndex("[]");
         /**
          * Special type name IDs for searching by tuple.
+         * @type {number}
          */
+        // @ts-expect-error
         this.typeNameIdOfTuple = this.buildTypeMapIndex("tuple");
         /**
          * Special type name IDs for searching by unit.
+         * @type {number}
          */
+        // @ts-expect-error
         this.typeNameIdOfUnit = this.buildTypeMapIndex("unit");
         /**
          * Special type name IDs for searching by both tuple and unit (`()` syntax).
+         * @type {number}
          */
+        // @ts-expect-error
         this.typeNameIdOfTupleOrUnit = this.buildTypeMapIndex("()");
         /**
          * Special type name IDs for searching `fn`.
+         * @type {number}
          */
+        // @ts-expect-error
         this.typeNameIdOfFn = this.buildTypeMapIndex("fn");
         /**
          * Special type name IDs for searching `fnmut`.
+         * @type {number}
          */
+        // @ts-expect-error
         this.typeNameIdOfFnMut = this.buildTypeMapIndex("fnmut");
         /**
          * Special type name IDs for searching `fnonce`.
+         * @type {number}
          */
+        // @ts-expect-error
         this.typeNameIdOfFnOnce = this.buildTypeMapIndex("fnonce");
         /**
          * Special type name IDs for searching higher order functions (`->` syntax).
+         * @type {number}
          */
+        // @ts-expect-error
         this.typeNameIdOfHof = this.buildTypeMapIndex("->");
         /**
          * Special type name IDs the output assoc type.
+         * @type {number}
          */
+        // @ts-expect-error
         this.typeNameIdOfOutput = this.buildTypeMapIndex("output", true);
         /**
          * Special type name IDs for searching by reference.
+         * @type {number}
          */
+        // @ts-expect-error
         this.typeNameIdOfReference = this.buildTypeMapIndex("reference");
 
         /**
          * Empty, immutable map used in item search types with no bindings.
          *
-         * @type {Map<number, Array<FunctionType>>}
+         * @type {Map<number, Array<any>>}
          */
         this.EMPTY_BINDINGS_MAP = new Map();
 
         /**
          * Empty, immutable map used in item search types with no bindings.
          *
-         * @type {Array<FunctionType>}
+         * @type {Array<any>}
          */
         this.EMPTY_GENERICS_ARRAY = [];
 
@@ -1403,7 +1565,7 @@ class DocSearch {
          * Object pool for function types with no bindings or generics.
          * This is reset after loading the index.
          *
-         * @type {Map<number|null, FunctionType>}
+         * @type {Map<number|null, rustdoc.FunctionType>}
          */
         this.TYPES_POOL = new Map();
 
@@ -1422,8 +1584,9 @@ class DocSearch {
         this.tailTable = new Map();
 
         /**
-         *  @type {Array<Row>}
+         *  @type {Array<rustdoc.Row>}
          */
+        // @ts-expect-error
         this.searchIndex = this.buildIndex(rawSearchIndex);
     }
 
@@ -1436,9 +1599,9 @@ class DocSearch {
      * get the same ID.
      *
      * @param {string} name
-     * @param {boolean} isAssocType - True if this is an assoc type
+     * @param {boolean=} isAssocType - True if this is an assoc type
      *
-     * @returns {integer}
+     * @returns {number?}
      */
     buildTypeMapIndex(name, isAssocType) {
         if (name === "" || name === null) {
@@ -1446,12 +1609,14 @@ class DocSearch {
         }
 
         if (this.typeNameIdMap.has(name)) {
+            /** @type {{id: number, assocOnly: boolean}} */
+            // @ts-expect-error
             const obj = this.typeNameIdMap.get(name);
-            obj.assocOnly = isAssocType && obj.assocOnly;
+            obj.assocOnly = !!(isAssocType && obj.assocOnly);
             return obj.id;
         } else {
             const id = this.typeNameIdMap.size;
-            this.typeNameIdMap.set(name, { id, assocOnly: isAssocType });
+            this.typeNameIdMap.set(name, { id, assocOnly: !!isAssocType });
             return id;
         }
     }
@@ -1469,13 +1634,26 @@ class DocSearch {
      * The format for individual function types is encoded in
      * librustdoc/html/render/mod.rs: impl Serialize for RenderType
      *
-     * @param {null|Array<RawFunctionType>} types
-     * @param {Array<{name: string, ty: number}>} lowercasePaths
+     * @param {null|Array<rustdoc.RawFunctionType>} types
+     * @param {Array<{
+     *     name: string,
+    *     ty: number,
+    *     path: string|null,
+    *     exactPath: string|null,
+    *     unboxFlag: boolean
+    * }>} paths
+    * @param {Array<{
+    *     name: string,
+    *     ty: number,
+    *     path: string|null,
+    *     exactPath: string|null,
+    *     unboxFlag: boolean,
+    * }>} lowercasePaths
      *
-     * @return {Array<FunctionSearchType>}
+     * @return {Array<rustdoc.FunctionType>}
      */
     buildItemSearchTypeAll(types, paths, lowercasePaths) {
-        return types.length > 0 ?
+        return types && types.length > 0 ?
             types.map(type => this.buildItemSearchType(type, paths, lowercasePaths)) :
             this.EMPTY_GENERICS_ARRAY;
     }
@@ -1483,7 +1661,22 @@ class DocSearch {
     /**
      * Converts a single type.
      *
-     * @param {RawFunctionType} type
+     * @param {rustdoc.RawFunctionType} type
+     * @param {Array<{
+     *     name: string,
+     *     ty: number,
+     *     path: string|null,
+     *     exactPath: string|null,
+     *     unboxFlag: boolean
+     * }>} paths
+     * @param {Array<{
+     *     name: string,
+     *     ty: number,
+     *     path: string|null,
+     *     exactPath: string|null,
+     *     unboxFlag: boolean,
+     * }>} lowercasePaths
+     * @param {boolean=} isAssocType
      */
     buildItemSearchType(type, paths, lowercasePaths, isAssocType) {
         const PATH_INDEX_DATA = 0;
@@ -1501,7 +1694,9 @@ class DocSearch {
                 paths,
                 lowercasePaths,
             );
+            // @ts-expect-error
             if (type.length > BINDINGS_DATA && type[BINDINGS_DATA].length > 0) {
+                // @ts-expect-error
                 bindings = new Map(type[BINDINGS_DATA].map(binding => {
                     const [assocType, constraints] = binding;
                     // Associated type constructors are represented sloppily in rustdoc's
@@ -1524,7 +1719,7 @@ class DocSearch {
             }
         }
         /**
-         * @type {FunctionType}
+         * @type {rustdoc.FunctionType}
          */
         let result;
         if (pathIndex < 0) {
@@ -1555,7 +1750,7 @@ class DocSearch {
         } else {
             const item = lowercasePaths[pathIndex - 1];
             const id = this.buildTypeMapIndex(item.name, isAssocType);
-            if (isAssocType) {
+            if (isAssocType && id !== null) {
                 this.assocTypeIdNameMap.set(id, paths[pathIndex - 1].name);
             }
             result = {
@@ -1585,6 +1780,7 @@ class DocSearch {
             if (cr.bindings.size === result.bindings.size && cr.bindings !== result.bindings) {
                 let ok = true;
                 for (const [k, v] of cr.bindings.entries()) {
+                    // @ts-expect-error
                     const v2 = result.bindings.get(v);
                     if (!v2) {
                         ok = false;
@@ -1629,7 +1825,7 @@ class DocSearch {
      * [^1]: Distance is the relatively naive metric of counting the number of distinct items in
      * the function that are not present in the query.
      *
-     * @param {FunctionType|QueryElement} type - a single type
+     * @param {rustdoc.FingerprintableType} type - a single type
      * @param {Uint32Array} output - write the fingerprint to this data structure: uses 128 bits
      */
     buildFunctionTypeFingerprint(type, output) {
@@ -1647,9 +1843,12 @@ class DocSearch {
             input === this.typeNameIdOfFnOnce) {
             input = this.typeNameIdOfHof;
         }
-        // http://burtleburtle.net/bob/hash/integer.html
-        // ~~ is toInt32. It's used before adding, so
-        // the number stays in safe integer range.
+        /**
+         * http://burtleburtle.net/bob/hash/integer.html
+         * ~~ is toInt32. It's used before adding, so
+         * the number stays in safe integer range.
+         * @param {number} k
+         */
         const hashint1 = k => {
             k = (~~k + 0x7ed55d16) + (k << 12);
             k = (k ^ 0xc761c23c) ^ (k >>> 19);
@@ -1658,6 +1857,7 @@ class DocSearch {
             k = (~~k + 0xfd7046c5) + (k << 3);
             return (k ^ 0xb55a4f09) ^ (k >>> 16);
         };
+        /** @param {number} k */
         const hashint2 = k => {
             k = ~k + (k << 15);
             k ^= k >>> 12;
@@ -1684,6 +1884,14 @@ class DocSearch {
         for (const g of type.generics) {
             this.buildFunctionTypeFingerprint(g, output);
         }
+        /**
+         * @type {{
+         *   id: number|null,
+         *   ty: number,
+         *   generics: rustdoc.FingerprintableType[],
+         *   bindings: Map<number, rustdoc.FingerprintableType[]>
+         * }}
+         */
         const fb = {
             id: null,
             ty: 0,
@@ -1700,7 +1908,7 @@ class DocSearch {
     /**
      * Convert raw search index into in-memory search index.
      *
-     * @param {[string, RawSearchIndexCrate][]} rawSearchIndex
+     * @param {Map<string, rustdoc.RawSearchIndexCrate>} rawSearchIndex
      */
     buildIndex(rawSearchIndex) {
         /**
@@ -1714,19 +1922,37 @@ class DocSearch {
          * The raw function search type format is generated using serde in
          * librustdoc/html/render/mod.rs: IndexItemFunctionType::write_to_string
          *
-         * @param {Array<{name: string, ty: number}>} paths
-         * @param {Array<{name: string, ty: number}>} lowercasePaths
+         * @param {Array<{
+         *     name: string,
+         *     ty: number,
+         *     path: string|null,
+         *     exactPath: string|null,
+         *     unboxFlag: boolean
+         * }>} paths
+         * @param {Array<{
+         *     name: string,
+         *     ty: number,
+         *     path: string|null,
+         *     exactPath: string|null,
+         *     unboxFlag: boolean
+         * }>} lowercasePaths
          *
-         * @return {null|FunctionSearchType}
+         * @return {function(rustdoc.RawFunctionSearchType): null|rustdoc.FunctionSearchType}
          */
         const buildFunctionSearchTypeCallback = (paths, lowercasePaths) => {
-            return functionSearchType => {
+            /**
+             * @param {rustdoc.RawFunctionSearchType} functionSearchType
+             */
+            const cb = functionSearchType => {
                 if (functionSearchType === 0) {
                     return null;
                 }
                 const INPUTS_DATA = 0;
                 const OUTPUT_DATA = 1;
-                let inputs, output;
+                /** @type {rustdoc.FunctionType[]} */
+                let inputs;
+                /** @type {rustdoc.FunctionType[]} */
+                let output;
                 if (typeof functionSearchType[INPUTS_DATA] === "number") {
                     inputs = [
                         this.buildItemSearchType(
@@ -1753,6 +1979,7 @@ class DocSearch {
                         ];
                     } else {
                         output = this.buildItemSearchTypeAll(
+                            // @ts-expect-error
                             functionSearchType[OUTPUT_DATA],
                             paths,
                             lowercasePaths,
@@ -1765,8 +1992,10 @@ class DocSearch {
                 const l = functionSearchType.length;
                 for (let i = 2; i < l; ++i) {
                     where_clause.push(typeof functionSearchType[i] === "number"
+                        // @ts-expect-error
                         ? [this.buildItemSearchType(functionSearchType[i], paths, lowercasePaths)]
                         : this.buildItemSearchTypeAll(
+                            // @ts-expect-error
                             functionSearchType[i],
                             paths,
                             lowercasePaths,
@@ -1776,6 +2005,7 @@ class DocSearch {
                     inputs, output, where_clause,
                 };
             };
+            return cb;
         };
 
         const searchIndex = [];
@@ -1798,7 +2028,12 @@ class DocSearch {
         for (const [crate, crateCorpus] of rawSearchIndex) {
             // a string representing the lengths of each description shard
             // a string representing the list of function types
-            const itemDescShardDecoder = new VlqHexDecoder(crateCorpus.D, noop => noop);
+            const itemDescShardDecoder = new VlqHexDecoder(crateCorpus.D, noop => {
+                /** @type {number} */
+                // @ts-expect-error
+                const n = noop;
+                return n;
+            });
             let descShard = {
                 crate,
                 shard: 0,
@@ -1817,7 +2052,7 @@ class DocSearch {
             /**
              * List of generic function type parameter names.
              * Used for display, not for searching.
-             * @type {[string]}
+             * @type {string[]}
              */
             let lastParamNames = [];
 
@@ -1847,6 +2082,8 @@ class DocSearch {
             id += 1;
             searchIndex.push(crateRow);
             currentIndex += 1;
+            // it's not undefined
+            // @ts-expect-error
             if (!this.searchIndexEmptyDesc.get(crate).contains(0)) {
                 descIndex += 1;
             }
@@ -1870,7 +2107,7 @@ class DocSearch {
             const implDisambiguator = new Map(crateCorpus.b);
             // an array of [(Number) item type,
             //              (String) name]
-            const paths = crateCorpus.p;
+            const rawPaths = crateCorpus.p;
             // an array of [(String) alias name
             //             [Number] index to items]
             const aliases = crateCorpus.a;
@@ -1879,33 +2116,61 @@ class DocSearch {
             // an item whose index is not present will fall back to the previous present path
             const itemParamNames = new Map(crateCorpus.P);
 
-            // an array of [{name: String, ty: Number}]
+            /**
+             * @type {Array<{
+             *     name: string,
+             *     ty: number,
+             *     path: string|null,
+             *     exactPath: string|null,
+             *     unboxFlag: boolean
+             * }>}
+             */
             const lowercasePaths = [];
+            /**
+             * @type {Array<{
+             *     name: string,
+             *     ty: number,
+             *     path: string|null,
+             *     exactPath: string|null,
+             *     unboxFlag: boolean
+             * }>}
+             */
+            const paths = [];
 
             // a string representing the list of function types
             const itemFunctionDecoder = new VlqHexDecoder(
                 crateCorpus.f,
+                // @ts-expect-error
                 buildFunctionSearchTypeCallback(paths, lowercasePaths),
             );
 
             // convert `rawPaths` entries into object form
             // generate normalizedPaths for function search mode
-            let len = paths.length;
+            let len = rawPaths.length;
             let lastPath = itemPaths.get(0);
             for (let i = 0; i < len; ++i) {
-                const elem = paths[i];
+                const elem = rawPaths[i];
                 const ty = elem[0];
                 const name = elem[1];
                 let path = null;
                 if (elem.length > 2 && elem[2] !== null) {
+                    // @ts-expect-error
                     path = itemPaths.has(elem[2]) ? itemPaths.get(elem[2]) : lastPath;
                     lastPath = path;
                 }
-                const exactPath = elem.length > 3 && elem[3] !== null ?
+                let exactPath = elem.length > 3 && elem[3] !== null ?
+                    // @ts-expect-error
                     itemPaths.get(elem[3]) :
                     path;
                 const unboxFlag = elem.length > 4 && !!elem[4];
 
+                if (path === undefined) {
+                    path = null;
+                }
+                if (exactPath === undefined) {
+                    exactPath = null;
+                }
+
                 lowercasePaths.push({ ty, name: name.toLowerCase(), path, exactPath, unboxFlag });
                 paths[i] = { ty, name, path, exactPath, unboxFlag };
             }
@@ -1924,6 +2189,7 @@ class DocSearch {
             for (let i = 0; i < len; ++i) {
                 const bitIndex = i + 1;
                 if (descIndex >= descShard.len &&
+                    // @ts-expect-error
                     !this.searchIndexEmptyDesc.get(crate).contains(bitIndex)) {
                     descShard = {
                         crate,
@@ -1938,8 +2204,11 @@ class DocSearch {
                 }
                 const name = itemNames[i] === "" ? lastName : itemNames[i];
                 const word = itemNames[i] === "" ? lastWord : itemNames[i].toLowerCase();
+                /** @type {string} */
+                // @ts-expect-error
                 const path = itemPaths.has(i) ? itemPaths.get(i) : lastPath;
                 const paramNames = itemParamNames.has(i) ?
+                    // @ts-expect-error
                     itemParamNames.get(i).split(",") :
                     lastParamNames;
                 const type = itemFunctionDecoder.next();
@@ -1971,7 +2240,9 @@ class DocSearch {
                     descShard,
                     descIndex,
                     exactPath: itemReexports.has(i) ?
+                        // @ts-expect-error
                         itemPaths.get(itemReexports.get(i)) : path,
+                    // @ts-expect-error
                     parent: itemParentIdx > 0 ? paths[itemParentIdx - 1] : undefined,
                     type,
                     paramNames,
@@ -1987,6 +2258,7 @@ class DocSearch {
                 searchIndex.push(row);
                 lastPath = row.path;
                 lastParamNames = row.paramNames;
+                // @ts-expect-error
                 if (!this.searchIndexEmptyDesc.get(crate).contains(bitIndex)) {
                     descIndex += 1;
                 }
@@ -2002,13 +2274,16 @@ class DocSearch {
                         continue;
                     }
 
+                    // @ts-expect-error
                     let currentNameAliases;
                     if (currentCrateAliases.has(alias_name)) {
                         currentNameAliases = currentCrateAliases.get(alias_name);
                     } else {
                         currentNameAliases = [];
+                        // @ts-expect-error
                         currentCrateAliases.set(alias_name, currentNameAliases);
                     }
+                    // @ts-expect-error
                     for (const local_alias of aliases[alias_name]) {
                         currentNameAliases.push(local_alias + currentIndex);
                     }
@@ -2030,11 +2305,15 @@ class DocSearch {
      *
      * When adding new things to the parser, add them there, too!
      *
-     * @param  {string} val     - The user query
+     * @param  {string} userQuery - The user query
      *
-     * @return {ParsedQuery}    - The parsed query
+     * @return {rustdoc.ParsedQuery<rustdoc.ParserQueryElement>} - The parsed query
      */
     static parseQuery(userQuery) {
+        /**
+         * @param {string} typename
+         * @returns {number}
+         */
         function itemTypeFromName(typename) {
             const index = itemTypes.findIndex(i => i === typename);
             if (index < 0) {
@@ -2043,14 +2322,19 @@ class DocSearch {
             return index;
         }
 
+        /**
+         * @param {rustdoc.ParserQueryElement} elem
+         */
         function convertTypeFilterOnElem(elem) {
             if (elem.typeFilter !== null) {
                 let typeFilter = elem.typeFilter;
                 if (typeFilter === "const") {
                     typeFilter = "constant";
                 }
+                // @ts-expect-error
                 elem.typeFilter = itemTypeFromName(typeFilter);
             } else {
+                // @ts-expect-error
                 elem.typeFilter = NO_TYPE_FILTER;
             }
             for (const elem2 of elem.generics) {
@@ -2068,7 +2352,7 @@ class DocSearch {
          *
          * @param {string} userQuery
          *
-         * @return {ParsedQuery}
+         * @return {rustdoc.ParsedQuery<rustdoc.ParserQueryElement>}
          */
         function newParsedQuery(userQuery) {
             return {
@@ -2094,8 +2378,8 @@ class DocSearch {
         * Parses the provided `query` input to fill `parserState`. If it encounters an error while
         * parsing `query`, it'll throw an error.
         *
-        * @param {ParsedQuery} query
-        * @param {ParserState} parserState
+        * @param {rustdoc.ParsedQuery<rustdoc.ParserQueryElement>} query
+        * @param {rustdoc.ParserState} parserState
         */
         function parseInput(query, parserState) {
             let foundStopChar = true;
@@ -2125,6 +2409,7 @@ class DocSearch {
                 if (!foundStopChar) {
                     let extra = "";
                     if (isLastElemGeneric(query.elems, parserState)) {
+                        // @ts-expect-error
                         extra = [" after ", ">"];
                     } else if (prevIs(parserState, "\"")) {
                         throw ["Cannot have more than one element if you use quotes"];
@@ -2208,6 +2493,8 @@ class DocSearch {
             }
         } catch (err) {
             query = newParsedQuery(userQuery);
+            // is string list
+            // @ts-expect-error
             query.error = err;
             return query;
         }
@@ -2224,25 +2511,167 @@ class DocSearch {
     /**
      * Executes the parsed query and builds a {ResultsTable}.
      *
-     * @param  {ParsedQuery} parsedQuery - The parsed user query
-     * @param  {Object} [filterCrates]   - Crate to search in if defined
-     * @param  {Object} [currentCrate]   - Current crate, to rank results from this crate higher
+     * @param  {rustdoc.ParsedQuery<rustdoc.ParserQueryElement>} origParsedQuery
+     *     - The parsed user query
+     * @param  {Object} [filterCrates] - Crate to search in if defined
+     * @param  {Object} [currentCrate] - Current crate, to rank results from this crate higher
      *
-     * @return {ResultsTable}
+     * @return {Promise<rustdoc.ResultsTable>}
      */
-    async execQuery(parsedQuery, filterCrates, currentCrate) {
+    async execQuery(origParsedQuery, filterCrates, currentCrate) {
         const results_others = new Map(), results_in_args = new Map(),
             results_returned = new Map();
 
+        /** @type {rustdoc.ParsedQuery<rustdoc.QueryElement>} */
+        // @ts-expect-error
+        const parsedQuery = origParsedQuery;
+
+        const queryLen =
+            parsedQuery.elems.reduce((acc, next) => acc + next.pathLast.length, 0) +
+            parsedQuery.returned.reduce((acc, next) => acc + next.pathLast.length, 0);
+        const maxEditDistance = Math.floor(queryLen / 3);
+
+        /**
+         * @type {Map<string, number>}
+         */
+        const genericSymbols = new Map();
+
+        /**
+         * Convert names to ids in parsed query elements.
+         * This is not used for the "In Names" tab, but is used for the
+         * "In Params", "In Returns", and "In Function Signature" tabs.
+         *
+         * If there is no matching item, but a close-enough match, this
+         * function also that correction.
+         *
+         * See `buildTypeMapIndex` for more information.
+         *
+         * @param {rustdoc.QueryElement} elem
+         * @param {boolean} isAssocType
+         */
+        const convertNameToId = (elem, isAssocType) => {
+            const loweredName = elem.pathLast.toLowerCase();
+            if (this.typeNameIdMap.has(loweredName) &&
+                // @ts-expect-error
+                (isAssocType || !this.typeNameIdMap.get(loweredName).assocOnly)) {
+                // @ts-expect-error
+                elem.id = this.typeNameIdMap.get(loweredName).id;
+            } else if (!parsedQuery.literalSearch) {
+                let match = null;
+                let matchDist = maxEditDistance + 1;
+                let matchName = "";
+                for (const [name, { id, assocOnly }] of this.typeNameIdMap) {
+                    const dist = Math.min(
+                        editDistance(name, loweredName, maxEditDistance),
+                        editDistance(name, elem.normalizedPathLast, maxEditDistance),
+                    );
+                    if (dist <= matchDist && dist <= maxEditDistance &&
+                        (isAssocType || !assocOnly)) {
+                        if (dist === matchDist && matchName > name) {
+                            continue;
+                        }
+                        match = id;
+                        matchDist = dist;
+                        matchName = name;
+                    }
+                }
+                if (match !== null) {
+                    parsedQuery.correction = matchName;
+                }
+                elem.id = match;
+            }
+            if ((elem.id === null && parsedQuery.totalElems > 1 && elem.typeFilter === -1
+                && elem.generics.length === 0 && elem.bindings.size === 0)
+                || elem.typeFilter === TY_GENERIC) {
+                if (genericSymbols.has(elem.normalizedPathLast)) {
+                    // @ts-expect-error
+                    elem.id = genericSymbols.get(elem.normalizedPathLast);
+                } else {
+                    elem.id = -(genericSymbols.size + 1);
+                    genericSymbols.set(elem.normalizedPathLast, elem.id);
+                }
+                if (elem.typeFilter === -1 && elem.normalizedPathLast.length >= 3) {
+                    // Silly heuristic to catch if the user probably meant
+                    // to not write a generic parameter. We don't use it,
+                    // just bring it up.
+                    const maxPartDistance = Math.floor(elem.normalizedPathLast.length / 3);
+                    let matchDist = maxPartDistance + 1;
+                    let matchName = "";
+                    for (const name of this.typeNameIdMap.keys()) {
+                        const dist = editDistance(
+                            name,
+                            elem.normalizedPathLast,
+                            maxPartDistance,
+                        );
+                        if (dist <= matchDist && dist <= maxPartDistance) {
+                            if (dist === matchDist && matchName > name) {
+                                continue;
+                            }
+                            matchDist = dist;
+                            matchName = name;
+                        }
+                    }
+                    if (matchName !== "") {
+                        parsedQuery.proposeCorrectionFrom = elem.name;
+                        parsedQuery.proposeCorrectionTo = matchName;
+                    }
+                }
+                elem.typeFilter = TY_GENERIC;
+            }
+            if (elem.generics.length > 0 && elem.typeFilter === TY_GENERIC) {
+                // Rust does not have HKT
+                parsedQuery.error = [
+                    "Generic type parameter ",
+                    elem.name,
+                    " does not accept generic parameters",
+                ];
+            }
+            for (const elem2 of elem.generics) {
+                // @ts-expect-error
+                convertNameToId(elem2);
+            }
+            elem.bindings = new Map(Array.from(elem.bindings.entries())
+                .map(entry => {
+                    const [name, constraints] = entry;
+                    // @ts-expect-error
+                    if (!this.typeNameIdMap.has(name)) {
+                        parsedQuery.error = [
+                            "Type parameter ",
+                            // @ts-expect-error
+                            name,
+                            " does not exist",
+                        ];
+                        return [0, []];
+                    }
+                    for (const elem2 of constraints) {
+                        convertNameToId(elem2, false);
+                    }
+
+                    // @ts-expect-error
+                    return [this.typeNameIdMap.get(name).id, constraints];
+                }),
+            );
+        };
+
+        for (const elem of parsedQuery.elems) {
+            convertNameToId(elem, false);
+            this.buildFunctionTypeFingerprint(elem, parsedQuery.typeFingerprint);
+        }
+        for (const elem of parsedQuery.returned) {
+            convertNameToId(elem, false);
+            this.buildFunctionTypeFingerprint(elem, parsedQuery.typeFingerprint);
+        }
+
+
         /**
          * Creates the query results.
          *
-         * @param {Array<Result>} results_in_args
-         * @param {Array<Result>} results_returned
-         * @param {Array<Result>} results_others
-         * @param {ParsedQuery} parsedQuery
+         * @param {Array<rustdoc.ResultObject>} results_in_args
+         * @param {Array<rustdoc.ResultObject>} results_returned
+         * @param {Array<rustdoc.ResultObject>} results_others
+         * @param {rustdoc.ParsedQuery<rustdoc.QueryElement>} parsedQuery
          *
-         * @return {ResultsTable}
+         * @return {rustdoc.ResultsTable}
          */
         function createQueryResults(
             results_in_args,
@@ -2257,6 +2686,7 @@ class DocSearch {
             };
         }
 
+        // @ts-expect-error
         const buildHrefAndPath = item => {
             let displayPath;
             let href;
@@ -2320,6 +2750,7 @@ class DocSearch {
             return [displayPath, href, `${exactPath}::${name}`];
         };
 
+        // @ts-expect-error
         function pathSplitter(path) {
             const tmp = "<span>" + path.replace(/::/g, "::</span><span>");
             if (tmp.endsWith("<span>")) {
@@ -2332,10 +2763,9 @@ class DocSearch {
          * Add extra data to result objects, and filter items that have been
          * marked for removal.
          *
-         * @param {[ResultObject]} results
+         * @param {[rustdoc.ResultObject]} results
          * @param {"sig"|"elems"|"returned"|null} typeInfo
-         * @param {ParsedQuery} query
-         * @returns {[ResultObject]}
+         * @returns {[rustdoc.ResultObject]}
          */
         const transformResults = (results, typeInfo) => {
             const duplicates = new Set();
@@ -2350,7 +2780,9 @@ class DocSearch {
                     }, this.searchIndex[result.id]);
 
                     // To be sure than it some items aren't considered as duplicate.
+                    // @ts-expect-error
                     obj.fullPath = res[2] + "|" + obj.ty;
+                    // @ts-expect-error
                     if (duplicates.has(obj.fullPath)) {
                         continue;
                     }
@@ -2363,14 +2795,18 @@ class DocSearch {
                     if (duplicates.has(res[2] + "|" + TY_IMPORT)) {
                         continue;
                     }
+                    // @ts-expect-error
                     duplicates.add(obj.fullPath);
                     duplicates.add(res[2]);
 
                     if (typeInfo !== null) {
+                        // @ts-expect-error
                         obj.displayTypeSignature =
+                            // @ts-expect-error
                             this.formatDisplayTypeSignature(obj, typeInfo);
                     }
 
+                    // @ts-expect-error
                     obj.href = res[1];
                     out.push(obj);
                     if (out.length >= MAX_RESULTS) {
@@ -2378,6 +2814,7 @@ class DocSearch {
                     }
                 }
             }
+            // @ts-expect-error
             return out;
         };
 
@@ -2388,30 +2825,34 @@ class DocSearch {
          * The output is formatted as an array of hunks, where odd numbered
          * hunks are highlighted and even numbered ones are not.
          *
-         * @param {ResultObject} obj
+         * @param {rustdoc.ResultObject} obj
          * @param {"sig"|"elems"|"returned"|null} typeInfo
-         * @param {ParsedQuery} query
-         * @returns Promise<
+         * @returns {Promise<{
          *   "type": Array<string>,
          *   "mappedNames": Map<string, string>,
          *   "whereClause": Map<string, Array<string>>,
-         * >
+         * }>}
          */
         this.formatDisplayTypeSignature = async(obj, typeInfo) => {
+            const objType = obj.type;
+            if (!objType) {
+                return {type: [], mappedNames: new Map(), whereClause: new Map()};
+            }
             let fnInputs = null;
             let fnOutput = null;
+            // @ts-expect-error
             let mgens = null;
             if (typeInfo !== "elems" && typeInfo !== "returned") {
                 fnInputs = unifyFunctionTypes(
-                    obj.type.inputs,
+                    objType.inputs,
                     parsedQuery.elems,
-                    obj.type.where_clause,
+                    objType.where_clause,
                     null,
                     mgensScratch => {
                         fnOutput = unifyFunctionTypes(
-                            obj.type.output,
+                            objType.output,
                             parsedQuery.returned,
-                            obj.type.where_clause,
+                            objType.where_clause,
                             mgensScratch,
                             mgensOut => {
                                 mgens = mgensOut;
@@ -2424,11 +2865,11 @@ class DocSearch {
                     0,
                 );
             } else {
-                const arr = typeInfo === "elems" ? obj.type.inputs : obj.type.output;
+                const arr = typeInfo === "elems" ? objType.inputs : objType.output;
                 const highlighted = unifyFunctionTypes(
                     arr,
                     parsedQuery.elems,
-                    obj.type.where_clause,
+                    objType.where_clause,
                     null,
                     mgensOut => {
                         mgens = mgensOut;
@@ -2443,15 +2884,16 @@ class DocSearch {
                 }
             }
             if (!fnInputs) {
-                fnInputs = obj.type.inputs;
+                fnInputs = objType.inputs;
             }
             if (!fnOutput) {
-                fnOutput = obj.type.output;
+                fnOutput = objType.output;
             }
             const mappedNames = new Map();
             const whereClause = new Map();
 
-            const fnParamNames = obj.paramNames;
+            const fnParamNames = obj.paramNames || [];
+            // @ts-expect-error
             const queryParamNames = [];
             /**
              * Recursively writes a map of IDs to query generic names,
@@ -2461,10 +2903,10 @@ class DocSearch {
              * mapping `(-1, "X")`, and the writeFn function looks up the entry
              * for -1 to form the final, user-visible mapping of "X is T".
              *
-             * @param {QueryElement} queryElem
+             * @param {rustdoc.QueryElement} queryElem
              */
             const remapQuery = queryElem => {
-                if (queryElem.id < 0) {
+                if (queryElem.id !== null && queryElem.id < 0) {
                     queryParamNames[-1 - queryElem.id] = queryElem.name;
                 }
                 if (queryElem.generics.length > 0) {
@@ -2483,7 +2925,7 @@ class DocSearch {
              * Index 0 is not highlighted, index 1 is highlighted,
              * index 2 is not highlighted, etc.
              *
-             * @param {{name: string, highlighted: bool|undefined}} fnType - input
+             * @param {{name?: string, highlighted?: boolean}} fnType - input
              * @param {[string]} result
              */
             const pushText = (fnType, result) => {
@@ -2496,6 +2938,7 @@ class DocSearch {
                 // needs coerced to a boolean.
                 if (!!(result.length % 2) === !!fnType.highlighted) {
                     result.push("");
+                // @ts-expect-error
                 } else if (result.length === 0 && !!fnType.highlighted) {
                     result.push("");
                     result.push("");
@@ -2508,7 +2951,7 @@ class DocSearch {
              * Write a higher order function type: either a function pointer
              * or a trait bound on Fn, FnMut, or FnOnce.
              *
-             * @param {FunctionType} fnType - input
+             * @param {rustdoc.HighlightedFunctionType} fnType - input
              * @param {[string]} result
              */
             const writeHof = (fnType, result) => {
@@ -2548,7 +2991,7 @@ class DocSearch {
              * Write a primitive type with special syntax, like `!` or `[T]`.
              * Returns `false` if the supplied type isn't special.
              *
-             * @param {FunctionType} fnType
+             * @param {rustdoc.HighlightedFunctionType} fnType
              * @param {[string]} result
              */
             const writeSpecialPrimitive = (fnType, result) => {
@@ -2563,6 +3006,7 @@ class DocSearch {
                     onEachBtwn(
                         fnType.generics,
                         nested => writeFn(nested, result),
+                        // @ts-expect-error
                         () => pushText({ name: ", ", highlighted: false }, result),
                     );
                     pushText({ name: sb, highlighted: fnType.highlighted }, result);
@@ -2573,9 +3017,10 @@ class DocSearch {
                     onEachBtwn(
                         fnType.generics,
                         value => {
-                            prevHighlighted = value.highlighted;
+                            prevHighlighted = !!value.highlighted;
                             writeFn(value, result);
                         },
+                        // @ts-expect-error
                         value => pushText({
                             name: " ",
                             highlighted: prevHighlighted && value.highlighted,
@@ -2593,25 +3038,27 @@ class DocSearch {
              * like slices, with their own formatting. It also handles
              * updating the where clause and generic type param map.
              *
-             * @param {FunctionType} fnType
+             * @param {rustdoc.HighlightedFunctionType} fnType
              * @param {[string]} result
              */
             const writeFn = (fnType, result) => {
-                if (fnType.id < 0) {
+                if (fnType.id !== null && fnType.id < 0) {
                     if (fnParamNames[-1 - fnType.id] === "") {
                         // Normally, there's no need to shown an unhighlighted
                         // where clause, but if it's impl Trait, then we do.
                         const generics = fnType.generics.length > 0 ?
                             fnType.generics :
-                            obj.type.where_clause[-1 - fnType.id];
+                            objType.where_clause[-1 - fnType.id];
                         for (const nested of generics) {
                             writeFn(nested, result);
                         }
                         return;
+                    // @ts-expect-error
                     } else if (mgens) {
                         for (const [queryId, fnId] of mgens) {
                             if (fnId === fnType.id) {
                                 mappedNames.set(
+                                    // @ts-expect-error
                                     queryParamNames[-1 - queryId],
                                     fnParamNames[-1 - fnType.id],
                                 );
@@ -2622,13 +3069,17 @@ class DocSearch {
                         name: fnParamNames[-1 - fnType.id],
                         highlighted: !!fnType.highlighted,
                     }, result);
+                    // @ts-expect-error
                     const where = [];
                     onEachBtwn(
                         fnType.generics,
+                        // @ts-expect-error
                         nested => writeFn(nested, where),
+                        // @ts-expect-error
                         () => pushText({ name: " + ", highlighted: false }, where),
                     );
                     if (where.length > 0) {
+                        // @ts-expect-error
                         whereClause.set(fnParamNames[-1 - fnType.id], where);
                     }
                 } else {
@@ -2650,12 +3101,15 @@ class DocSearch {
                             fnType.bindings,
                             ([key, values]) => {
                                 const name = this.assocTypeIdNameMap.get(key);
+                                // @ts-expect-error
                                 if (values.length === 1 && values[0].id < 0 &&
+                                    // @ts-expect-error
                                     `${fnType.name}::${name}` === fnParamNames[-1 - values[0].id]) {
                                     // the internal `Item=Iterator::Item` type variable should be
                                     // shown in the where clause and name mapping output, but is
                                     // redundant in this spot
                                     for (const value of values) {
+                                        // @ts-expect-error
                                         writeFn(value, []);
                                     }
                                     return true;
@@ -2672,12 +3126,14 @@ class DocSearch {
                                 onEachBtwn(
                                     values || [],
                                     value => writeFn(value, result),
+                                    // @ts-expect-error
                                     () => pushText({ name: " + ",  highlighted: false }, result),
                                 );
                                 if (values.length !== 1) {
                                     pushText({ name: ")", highlighted: false }, result);
                                 }
                             },
+                            // @ts-expect-error
                             () => pushText({ name: ", ",  highlighted: false }, result),
                         );
                     }
@@ -2687,6 +3143,7 @@ class DocSearch {
                     onEachBtwn(
                         fnType.generics,
                         value => writeFn(value, result),
+                        // @ts-expect-error
                         () => pushText({ name: ", ",  highlighted: false }, result),
                     );
                     if (hasBindings || fnType.generics.length > 0) {
@@ -2694,19 +3151,26 @@ class DocSearch {
                     }
                 }
             };
+            // @ts-expect-error
             const type = [];
             onEachBtwn(
                 fnInputs,
+                // @ts-expect-error
                 fnType => writeFn(fnType, type),
+                // @ts-expect-error
                 () => pushText({ name: ", ",  highlighted: false }, type),
             );
+            // @ts-expect-error
             pushText({ name: " -> ", highlighted: false }, type);
             onEachBtwn(
                 fnOutput,
+                // @ts-expect-error
                 fnType => writeFn(fnType, type),
+                // @ts-expect-error
                 () => pushText({ name: ", ",  highlighted: false }, type),
             );
 
+            // @ts-expect-error
             return {type, mappedNames, whereClause};
         };
 
@@ -2714,10 +3178,10 @@ class DocSearch {
          * This function takes a result map, and sorts it by various criteria, including edit
          * distance, substring match, and the crate it comes from.
          *
-         * @param {Results} results
-         * @param {boolean} isType
+         * @param {rustdoc.Results} results
+         * @param {"sig"|"elems"|"returned"|null} typeInfo
          * @param {string} preferredCrate
-         * @returns {Promise<[ResultObject]>}
+         * @returns {Promise<[rustdoc.ResultObject]>}
          */
         const sortResults = async(results, typeInfo, preferredCrate) => {
             const userQuery = parsedQuery.userQuery;
@@ -2733,12 +3197,12 @@ class DocSearch {
                     // we are doing a return-type based search,
                     // deprioritize "clone-like" results,
                     // ie. functions that also take the queried type as an argument.
-                    const hasType = result.item && result.item.type;
-                    if (!hasType) {
+                    const resultItemType = result.item && result.item.type;
+                    if (!resultItemType) {
                         continue;
                     }
-                    const inputs = result.item.type.inputs;
-                    const where_clause = result.item.type.where_clause;
+                    const inputs = resultItemType.inputs;
+                    const where_clause = resultItemType.where_clause;
                     if (containsTypeFromQuery(inputs, where_clause)) {
                         result.path_dist *= 100;
                         result.dist *= 100;
@@ -2748,35 +3212,38 @@ class DocSearch {
             }
 
             result_list.sort((aaa, bbb) => {
-                let a, b;
+                /** @type {number} */
+                let a;
+                /** @type {number} */
+                let b;
 
                 // sort by exact case-sensitive match
                 if (isMixedCase) {
-                    a = (aaa.item.name !== userQuery);
-                    b = (bbb.item.name !== userQuery);
+                    a = Number(aaa.item.name !== userQuery);
+                    b = Number(bbb.item.name !== userQuery);
                     if (a !== b) {
                         return a - b;
                     }
                 }
 
                 // sort by exact match with regard to the last word (mismatch goes later)
-                a = (aaa.word !== normalizedUserQuery);
-                b = (bbb.word !== normalizedUserQuery);
+                a = Number(aaa.word !== normalizedUserQuery);
+                b = Number(bbb.word !== normalizedUserQuery);
                 if (a !== b) {
                     return a - b;
                 }
 
                 // sort by index of keyword in item name (no literal occurrence goes later)
-                a = (aaa.index < 0);
-                b = (bbb.index < 0);
+                a = Number(aaa.index < 0);
+                b = Number(bbb.index < 0);
                 if (a !== b) {
                     return a - b;
                 }
 
                 // in type based search, put functions first
                 if (parsedQuery.hasReturnArrow) {
-                    a = !isFnLikeTy(aaa.item.ty);
-                    b = !isFnLikeTy(bbb.item.ty);
+                    a = Number(!isFnLikeTy(aaa.item.ty));
+                    b = Number(!isFnLikeTy(bbb.item.ty));
                     if (a !== b) {
                         return a - b;
                     }
@@ -2784,80 +3251,93 @@ class DocSearch {
 
                 // Sort by distance in the path part, if specified
                 // (less changes required to match means higher rankings)
-                a = aaa.path_dist;
-                b = bbb.path_dist;
+                a = Number(aaa.path_dist);
+                b = Number(bbb.path_dist);
                 if (a !== b) {
                     return a - b;
                 }
 
                 // (later literal occurrence, if any, goes later)
-                a = aaa.index;
-                b = bbb.index;
+                a = Number(aaa.index);
+                b = Number(bbb.index);
                 if (a !== b) {
                     return a - b;
                 }
 
                 // Sort by distance in the name part, the last part of the path
                 // (less changes required to match means higher rankings)
-                a = (aaa.dist);
-                b = (bbb.dist);
+                a = Number(aaa.dist);
+                b = Number(bbb.dist);
                 if (a !== b) {
                     return a - b;
                 }
 
                 // sort deprecated items later
-                a = this.searchIndexDeprecated.get(aaa.item.crate).contains(aaa.item.bitIndex);
-                b = this.searchIndexDeprecated.get(bbb.item.crate).contains(bbb.item.bitIndex);
+                a = Number(
+                    // @ts-expect-error
+                    this.searchIndexDeprecated.get(aaa.item.crate).contains(aaa.item.bitIndex),
+                );
+                b = Number(
+                    // @ts-expect-error
+                    this.searchIndexDeprecated.get(bbb.item.crate).contains(bbb.item.bitIndex),
+                );
                 if (a !== b) {
                     return a - b;
                 }
 
                 // sort by crate (current crate comes first)
-                a = (aaa.item.crate !== preferredCrate);
-                b = (bbb.item.crate !== preferredCrate);
+                a = Number(aaa.item.crate !== preferredCrate);
+                b = Number(bbb.item.crate !== preferredCrate);
                 if (a !== b) {
                     return a - b;
                 }
 
                 // sort by item name length (longer goes later)
-                a = aaa.word.length;
-                b = bbb.word.length;
+                a = Number(aaa.word.length);
+                b = Number(bbb.word.length);
                 if (a !== b) {
                     return a - b;
                 }
 
                 // sort by item name (lexicographically larger goes later)
-                a = aaa.word;
-                b = bbb.word;
-                if (a !== b) {
-                    return (a > b ? +1 : -1);
+                let aw = aaa.word;
+                let bw = bbb.word;
+                if (aw !== bw) {
+                    return (aw > bw ? +1 : -1);
                 }
 
                 // sort by description (no description goes later)
-                a = this.searchIndexEmptyDesc.get(aaa.item.crate).contains(aaa.item.bitIndex);
-                b = this.searchIndexEmptyDesc.get(bbb.item.crate).contains(bbb.item.bitIndex);
+                a = Number(
+                    // @ts-expect-error
+                    this.searchIndexEmptyDesc.get(aaa.item.crate).contains(aaa.item.bitIndex),
+                );
+                b = Number(
+                    // @ts-expect-error
+                    this.searchIndexEmptyDesc.get(bbb.item.crate).contains(bbb.item.bitIndex),
+                );
                 if (a !== b) {
                     return a - b;
                 }
 
                 // sort by type (later occurrence in `itemTypes` goes later)
-                a = aaa.item.ty;
-                b = bbb.item.ty;
+                a = Number(aaa.item.ty);
+                b = Number(bbb.item.ty);
                 if (a !== b) {
                     return a - b;
                 }
 
                 // sort by path (lexicographically larger goes later)
-                a = aaa.item.path;
-                b = bbb.item.path;
-                if (a !== b) {
-                    return (a > b ? +1 : -1);
+                aw = aaa.item.path;
+                bw = bbb.item.path;
+                if (aw !== bw) {
+                    return (aw > bw ? +1 : -1);
                 }
 
                 // que sera, sera
                 return 0;
             });
 
+            // @ts-expect-error
             return transformResults(result_list, typeInfo);
         };
 
@@ -2871,17 +3351,19 @@ class DocSearch {
          * then this function will try with a different solution, or bail with null if it
          * runs out of candidates.
          *
-         * @param {Array<FunctionType>} fnTypesIn - The objects to check.
-         * @param {Array<QueryElement>} queryElems - The elements from the parsed query.
-         * @param {[FunctionType]} whereClause - Trait bounds for generic items.
+         * @param {rustdoc.FunctionType[]} fnTypesIn - The objects to check.
+         * @param {rustdoc.QueryElement[]} queryElems - The elements from the parsed query.
+         * @param {rustdoc.FunctionType[][]} whereClause - Trait bounds for generic items.
          * @param {Map<number,number>|null} mgensIn
          *     - Map query generics to function generics (never modified).
-         * @param {Map<number,number> -> bool} solutionCb - Called for each `mgens` solution.
+         * @param {function(Map<number,number>?): boolean} solutionCb
+         *     - Called for each `mgens` solution.
          * @param {number} unboxingDepth
          *     - Limit checks that Ty matches Vec<Ty>,
          *       but not Vec<ParamEnvAnd<WithInfcx<ConstTy<Interner<Ty=Ty>>>>>
          *
-         * @return {[FunctionType]|null} - Returns highlighted results if a match, null otherwise.
+         * @return {rustdoc.HighlightedFunctionType[]|null}
+         *     - Returns highlighted results if a match, null otherwise.
          */
         function unifyFunctionTypes(
             fnTypesIn,
@@ -2895,7 +3377,7 @@ class DocSearch {
                 return null;
             }
             /**
-             * @type Map<integer, integer>|null
+             * @type {Map<number, number>|null}
              */
             const mgens = mgensIn === null ? null : new Map(mgensIn);
             if (queryElems.length === 0) {
@@ -2915,7 +3397,11 @@ class DocSearch {
                     if (!unifyFunctionTypeIsMatchCandidate(fnType, queryElem, mgens)) {
                         continue;
                     }
-                    if (fnType.id < 0 && queryElem.id < 0) {
+                    if (fnType.id !== null &&
+                        fnType.id < 0 &&
+                        queryElem.id !== null &&
+                        queryElem.id < 0
+                    ) {
                         if (mgens && mgens.has(queryElem.id) &&
                             mgens.get(queryElem.id) !== fnType.id) {
                             continue;
@@ -2959,8 +3445,10 @@ class DocSearch {
                     )) {
                         continue;
                     }
+                    // @ts-expect-error
                     if (fnType.id < 0) {
                         const highlightedGenerics = unifyFunctionTypes(
+                            // @ts-expect-error
                             whereClause[(-fnType.id) - 1],
                             queryElems,
                             whereClause,
@@ -2998,12 +3486,13 @@ class DocSearch {
                         }
                     }
                 }
+                // @ts-expect-error
                 return false;
             }
 
             // Multiple element recursive case
             /**
-             * @type Array<FunctionType>
+             * @type {Array<rustdoc.FunctionType>}
              */
             const fnTypes = fnTypesIn.slice();
             /**
@@ -3033,7 +3522,7 @@ class DocSearch {
                     continue;
                 }
                 let mgensScratch;
-                if (fnType.id < 0) {
+                if (fnType.id !== null && queryElem.id !== null && fnType.id < 0) {
                     mgensScratch = new Map(mgens);
                     if (mgensScratch.has(queryElem.id)
                         && mgensScratch.get(queryElem.id) !== fnType.id) {
@@ -3052,8 +3541,11 @@ class DocSearch {
                 if (!queryElemsTmp) {
                     queryElemsTmp = queryElems.slice(0, qlast);
                 }
+                /** @type {rustdoc.HighlightedFunctionType[]|null} */
                 let unifiedGenerics = [];
+                // @ts-expect-error
                 let unifiedGenericsMgens = null;
+                /** @type {rustdoc.HighlightedFunctionType[]|null} */
                 const passesUnification = unifyFunctionTypes(
                     fnTypes,
                     queryElemsTmp,
@@ -3102,11 +3594,14 @@ class DocSearch {
                         bindings: new Map([...fnType.bindings.entries()].map(([k, v]) => {
                             return [k, queryElem.bindings.has(k) ? unifyFunctionTypes(
                                 v,
+                                // @ts-expect-error
                                 queryElem.bindings.get(k),
                                 whereClause,
+                                // @ts-expect-error
                                 unifiedGenericsMgens,
                                 solutionCb,
                                 unboxingDepth,
+                            // @ts-expect-error
                             ) : unifiedGenerics.splice(0, v.length)];
                         })),
                     });
@@ -3128,7 +3623,7 @@ class DocSearch {
                 )) {
                     continue;
                 }
-                const generics = fnType.id < 0 ?
+                const generics = fnType.id !== null && fnType.id < 0 ?
                     whereClause[(-fnType.id) - 1] :
                     fnType.generics;
                 const bindings = fnType.bindings ?
@@ -3171,17 +3666,19 @@ class DocSearch {
          * `Vec` of `Allocators` and not the implicit `Allocator` parameter that every
          * `Vec` has.
          *
-         * @param {Array<FunctionType>} fnTypesIn - The objects to check.
-         * @param {Array<QueryElement>} queryElems - The elements from the parsed query.
-         * @param {[FunctionType]} whereClause - Trait bounds for generic items.
+         * @param {Array<rustdoc.FunctionType>} fnTypesIn - The objects to check.
+         * @param {Array<rustdoc.QueryElement>} queryElems - The elements from the parsed query.
+         * @param {rustdoc.FunctionType[][]} whereClause - Trait bounds for generic items.
          * @param {Map<number,number>|null} mgensIn
          *     - Map functions generics to query generics (never modified).
-         * @param {Map<number,number> -> bool} solutionCb - Called for each `mgens` solution.
+         * @param {function(Map<number,number>): boolean} solutionCb
+         *     - Called for each `mgens` solution.
          * @param {number} unboxingDepth
          *     - Limit checks that Ty matches Vec<Ty>,
          *       but not Vec<ParamEnvAnd<WithInfcx<ConstTy<Interner<Ty=Ty>>>>>
          *
-         * @return {[FunctionType]|null} - Returns highlighted results if a match, null otherwise.
+         * @return {rustdoc.HighlightedFunctionType[]|null}
+         *     - Returns highlighted results if a match, null otherwise.
          */
         function unifyGenericTypes(
             fnTypesIn,
@@ -3195,10 +3692,11 @@ class DocSearch {
                 return null;
             }
             /**
-             * @type Map<integer, integer>|null
+             * @type {Map<number, number>|null}
              */
             const mgens = mgensIn === null ? null : new Map(mgensIn);
             if (queryElems.length === 0) {
+                // @ts-expect-error
                 return solutionCb(mgens) ? fnTypesIn : null;
             }
             if (!fnTypesIn || fnTypesIn.length === 0) {
@@ -3207,7 +3705,11 @@ class DocSearch {
             const fnType = fnTypesIn[0];
             const queryElem = queryElems[0];
             if (unifyFunctionTypeIsMatchCandidate(fnType, queryElem, mgens)) {
-                if (fnType.id < 0 && queryElem.id < 0) {
+                if (fnType.id !== null &&
+                    fnType.id < 0 &&
+                    queryElem.id !== null &&
+                    queryElem.id < 0
+                ) {
                     if (!mgens || !mgens.has(queryElem.id) ||
                         mgens.get(queryElem.id) === fnType.id
                     ) {
@@ -3238,6 +3740,7 @@ class DocSearch {
                         queryElems.slice(1),
                         whereClause,
                         mgens,
+                        // @ts-expect-error
                         mgensScratch => {
                             const solution = unifyFunctionTypeCheckBindings(
                                 fnType,
@@ -3285,7 +3788,7 @@ class DocSearch {
                 unboxingDepth + 1,
             )) {
                 let highlightedRemaining;
-                if (fnType.id < 0) {
+                if (fnType.id !== null && fnType.id < 0) {
                     // Where clause corresponds to `F: A + B`
                     //                                 ^^^^^
                     // The order of the constraints doesn't matter, so
@@ -3295,6 +3798,7 @@ class DocSearch {
                         [queryElem],
                         whereClause,
                         mgens,
+                        // @ts-expect-error
                         mgensScratch => {
                             const hl = unifyGenericTypes(
                                 fnTypesIn.slice(1),
@@ -3316,6 +3820,7 @@ class DocSearch {
                             highlighted: true,
                         }, fnType, {
                             generics: highlightedGenerics,
+                        // @ts-expect-error
                         }), ...highlightedRemaining];
                     }
                 } else {
@@ -3327,6 +3832,7 @@ class DocSearch {
                         [queryElem],
                         whereClause,
                         mgens,
+                        // @ts-expect-error
                         mgensScratch => {
                             const hl = unifyGenericTypes(
                                 fnTypesIn.slice(1),
@@ -3349,6 +3855,7 @@ class DocSearch {
                             bindings: new Map([...fnType.bindings.entries()].map(([k, v]) => {
                                 return [k, highlightedGenerics.splice(0, v.length)];
                             })),
+                        // @ts-expect-error
                         }), ...highlightedRemaining];
                     }
                 }
@@ -3364,8 +3871,8 @@ class DocSearch {
          * or associated type bindings: that's not load-bearing, but it prevents unnecessary
          * backtracking later.
          *
-         * @param {FunctionType} fnType
-         * @param {QueryElement} queryElem
+         * @param {rustdoc.FunctionType} fnType
+         * @param {rustdoc.QueryElement} queryElem
          * @param {Map<number,number>|null} mgensIn - Map query generics to function generics.
          * @returns {boolean}
          */
@@ -3377,7 +3884,7 @@ class DocSearch {
             // fnType.id < 0 means generic
             // queryElem.id < 0 does too
             // mgensIn[queryElem.id] = fnType.id
-            if (fnType.id < 0 && queryElem.id < 0) {
+            if (fnType.id !== null && fnType.id < 0 && queryElem.id !== null && queryElem.id < 0) {
                 if (
                     mgensIn && mgensIn.has(queryElem.id) &&
                     mgensIn.get(queryElem.id) !== fnType.id
@@ -3456,13 +3963,15 @@ class DocSearch {
          * ID of u32 in it, and the rest of the matching engine acts as if `Iterator<u32>` were
          * the type instead.
          *
-         * @param {FunctionType} fnType
-         * @param {QueryElement} queryElem
-         * @param {[FunctionType]} whereClause - Trait bounds for generic items.
-         * @param {Map<number,number>} mgensIn - Map query generics to function generics.
+         * @param {rustdoc.FunctionType} fnType
+         * @param {rustdoc.QueryElement} queryElem
+         * @param {rustdoc.FunctionType[][]} whereClause - Trait bounds for generic items.
+         * @param {Map<number,number>|null} mgensIn - Map query generics to function generics.
          *                                            Never modified.
          * @param {number} unboxingDepth
-         * @returns {false|{mgens: [Map<number,number>], simplifiedGenerics: [FunctionType]}}
+         * @returns {false|{
+         *     mgens: [Map<number,number>|null], simplifiedGenerics: rustdoc.FunctionType[]
+         * }}
          */
         function unifyFunctionTypeCheckBindings(
             fnType,
@@ -3486,8 +3995,10 @@ class DocSearch {
                     }
                     const fnTypeBindings = fnType.bindings.get(name);
                     mgensSolutionSet = mgensSolutionSet.flatMap(mgens => {
+                        // @ts-expect-error
                         const newSolutions = [];
                         unifyFunctionTypes(
+                            // @ts-expect-error
                             fnTypeBindings,
                             constraints,
                             whereClause,
@@ -3500,6 +4011,7 @@ class DocSearch {
                             },
                             unboxingDepth,
                         );
+                        // @ts-expect-error
                         return newSolutions;
                     });
                 }
@@ -3519,14 +4031,15 @@ class DocSearch {
                 } else {
                     simplifiedGenerics = binds;
                 }
+                // @ts-expect-error
                 return { simplifiedGenerics, mgens: mgensSolutionSet };
             }
             return { simplifiedGenerics, mgens: [mgensIn] };
         }
         /**
-         * @param {FunctionType} fnType
-         * @param {QueryElement} queryElem
-         * @param {[FunctionType]} whereClause - Trait bounds for generic items.
+         * @param {rustdoc.FunctionType} fnType
+         * @param {rustdoc.QueryElement} queryElem
+         * @param {rustdoc.FunctionType[][]} whereClause - Trait bounds for generic items.
          * @param {Map<number,number>|null} mgens - Map query generics to function generics.
          * @param {number} unboxingDepth
          * @returns {boolean}
@@ -3541,7 +4054,7 @@ class DocSearch {
             if (unboxingDepth >= UNBOXING_LIMIT) {
                 return false;
             }
-            if (fnType.id < 0) {
+            if (fnType.id !== null && fnType.id < 0) {
                 if (!whereClause) {
                     return false;
                 }
@@ -3577,14 +4090,14 @@ class DocSearch {
          * This function checks if the given list contains any
          * (non-generic) types mentioned in the query.
          *
-         * @param {Array<FunctionType>} list    - A list of function types.
-         * @param {[FunctionType]} where_clause - Trait bounds for generic items.
+         * @param {rustdoc.FunctionType[]} list    - A list of function types.
+         * @param {rustdoc.FunctionType[][]} where_clause - Trait bounds for generic items.
          */
         function containsTypeFromQuery(list, where_clause) {
             if (!list) return false;
             for (const ty of parsedQuery.returned) {
                 // negative type ids are generics
-                if (ty.id < 0) {
+                if (ty.id !== null && ty.id < 0) {
                     continue;
                 }
                 if (checkIfInList(list, ty, where_clause, null, 0)) {
@@ -3592,7 +4105,7 @@ class DocSearch {
                 }
             }
             for (const ty of parsedQuery.elems) {
-                if (ty.id < 0) {
+                if (ty.id !== null && ty.id < 0) {
                     continue;
                 }
                 if (checkIfInList(list, ty, where_clause, null, 0)) {
@@ -3606,9 +4119,9 @@ class DocSearch {
          * This function checks if the object (`row`) matches the given type (`elem`) and its
          * generics (if any).
          *
-         * @param {Array<FunctionType>} list
-         * @param {QueryElement} elem          - The element from the parsed query.
-         * @param {[FunctionType]} whereClause - Trait bounds for generic items.
+         * @param {rustdoc.FunctionType[]} list
+         * @param {rustdoc.QueryElement} elem          - The element from the parsed query.
+         * @param {rustdoc.FunctionType[][]} whereClause - Trait bounds for generic items.
          * @param {Map<number,number>|null} mgens - Map functions generics to query generics.
          * @param {number} unboxingDepth
          *
@@ -3627,18 +4140,20 @@ class DocSearch {
          * This function checks if the object (`row`) matches the given type (`elem`) and its
          * generics (if any).
          *
-         * @param {Row} row
-         * @param {QueryElement} elem          - The element from the parsed query.
-         * @param {[FunctionType]} whereClause - Trait bounds for generic items.
+         * @param {rustdoc.FunctionType} row
+         * @param {rustdoc.QueryElement} elem          - The element from the parsed query.
+         * @param {rustdoc.FunctionType[][]} whereClause - Trait bounds for generic items.
          * @param {Map<number,number>|null} mgens - Map query generics to function generics.
          *
          * @return {boolean} - Returns true if the type matches, false otherwise.
          */
+        // @ts-expect-error
         const checkType = (row, elem, whereClause, mgens, unboxingDepth) => {
             if (unboxingDepth >= UNBOXING_LIMIT) {
                 return false;
             }
-            if (row.id > 0 && elem.id > 0 && elem.pathWithoutLast.length === 0 &&
+            if (row.id !== null && elem.id !== null &&
+                row.id > 0 && elem.id > 0 && elem.pathWithoutLast.length === 0 &&
                 row.generics.length === 0 && elem.generics.length === 0 &&
                 row.bindings.size === 0 && elem.bindings.size === 0 &&
                 // special case
@@ -3648,6 +4163,7 @@ class DocSearch {
             ) {
                 return row.id === elem.id && typePassesFilter(elem.typeFilter, row.ty);
             } else {
+                // @ts-expect-error
                 return unifyFunctionTypes(
                     [row],
                     [elem],
@@ -3662,6 +4178,7 @@ class DocSearch {
         /**
          * Check a query solution for conflicting generics.
          */
+        // @ts-expect-error
         const checkTypeMgensForConflict = mgens => {
             if (!mgens) {
                 return true;
@@ -3679,7 +4196,7 @@ class DocSearch {
         /**
          * Compute an "edit distance" that ignores missing path elements.
          * @param {string[]} contains search query path
-         * @param {Row} ty indexed item
+         * @param {rustdoc.Row} ty indexed item
          * @returns {null|number} edit distance
          */
         function checkPath(contains, ty) {
@@ -3720,6 +4237,7 @@ class DocSearch {
             return ret_dist > maxPathEditDistance ? null : ret_dist;
         }
 
+        // @ts-expect-error
         function typePassesFilter(filter, type) {
             // No filter or Exact mach
             if (filter <= NO_TYPE_FILTER || filter === type) return true;
@@ -3741,6 +4259,7 @@ class DocSearch {
             return false;
         }
 
+        // @ts-expect-error
         function createAliasFromItem(item) {
             return {
                 crate: item.crate,
@@ -3758,11 +4277,14 @@ class DocSearch {
             };
         }
 
+        // @ts-expect-error
         const handleAliases = async(ret, query, filterCrates, currentCrate) => {
             const lowerQuery = query.toLowerCase();
             // We separate aliases and crate aliases because we want to have current crate
             // aliases to be before the others in the displayed results.
+            // @ts-expect-error
             const aliases = [];
+            // @ts-expect-error
             const crateAliases = [];
             if (filterCrates !== null) {
                 if (this.ALIASES.has(filterCrates)
@@ -3775,6 +4297,7 @@ class DocSearch {
             } else {
                 for (const [crate, crateAliasesIndex] of this.ALIASES) {
                     if (crateAliasesIndex.has(lowerQuery)) {
+                        // @ts-expect-error
                         const pushTo = crate === currentCrate ? crateAliases : aliases;
                         const query_aliases = crateAliasesIndex.get(lowerQuery);
                         for (const alias of query_aliases) {
@@ -3784,6 +4307,7 @@ class DocSearch {
                 }
             }
 
+            // @ts-expect-error
             const sortFunc = (aaa, bbb) => {
                 if (aaa.path < bbb.path) {
                     return 1;
@@ -3792,18 +4316,23 @@ class DocSearch {
                 }
                 return -1;
             };
+            // @ts-expect-error
             crateAliases.sort(sortFunc);
             aliases.sort(sortFunc);
 
+            // @ts-expect-error
             const fetchDesc = alias => {
+                // @ts-expect-error
                 return this.searchIndexEmptyDesc.get(alias.crate).contains(alias.bitIndex) ?
                     "" : this.searchState.loadDesc(alias);
             };
             const [crateDescs, descs] = await Promise.all([
+                // @ts-expect-error
                 Promise.all(crateAliases.map(fetchDesc)),
                 Promise.all(aliases.map(fetchDesc)),
             ]);
 
+            // @ts-expect-error
             const pushFunc = alias => {
                 alias.alias = query;
                 const res = buildHrefAndPath(alias);
@@ -3818,12 +4347,15 @@ class DocSearch {
             };
 
             aliases.forEach((alias, i) => {
+                // @ts-expect-error
                 alias.desc = descs[i];
             });
             aliases.forEach(pushFunc);
+            // @ts-expect-error
             crateAliases.forEach((alias, i) => {
                 alias.desc = crateDescs[i];
             });
+            // @ts-expect-error
             crateAliases.forEach(pushFunc);
         };
 
@@ -3843,21 +4375,24 @@ class DocSearch {
          * * `path_dist` is zero if a single-component search query is used, otherwise it's the
          *   distance computed for everything other than the last path component.
          *
-         * @param {Results} results
+         * @param {rustdoc.Results} results
          * @param {string} fullId
-         * @param {integer} id
-         * @param {integer} index
-         * @param {integer} dist
-         * @param {integer} path_dist
+         * @param {number} id
+         * @param {number} index
+         * @param {number} dist
+         * @param {number} path_dist
          */
+        // @ts-expect-error
         function addIntoResults(results, fullId, id, index, dist, path_dist, maxEditDistance) {
             if (dist <= maxEditDistance || index !== -1) {
                 if (results.has(fullId)) {
                     const result = results.get(fullId);
+                    // @ts-expect-error
                     if (result.dontValidate || result.dist <= dist) {
                         return;
                     }
                 }
+                // @ts-expect-error
                 results.set(fullId, {
                     id: id,
                     index: index,
@@ -3873,12 +4408,16 @@ class DocSearch {
          * try to match the items which validates all the elements. For `aa -> bb` will look for
          * functions which have a parameter `aa` and has `bb` in its returned values.
          *
-         * @param {Row} row
-         * @param {integer} pos      - Position in the `searchIndex`.
-         * @param {Object} results
+         * @param {rustdoc.Row} row
+         * @param {number} pos      - Position in the `searchIndex`.
+         * @param {rustdoc.Results} results
          */
         function handleArgs(row, pos, results) {
-            if (!row || (filterCrates !== null && row.crate !== filterCrates) || !row.type) {
+            if (!row || (filterCrates !== null && row.crate !== filterCrates)) {
+                return;
+            }
+            const rowType = row.type;
+            if (!rowType) {
                 return;
             }
 
@@ -3889,21 +4428,23 @@ class DocSearch {
             if (tfpDist === null) {
                 return;
             }
+            // @ts-expect-error
             if (results.size >= MAX_RESULTS && tfpDist > results.max_dist) {
                 return;
             }
 
             // If the result is too "bad", we return false and it ends this search.
             if (!unifyFunctionTypes(
-                row.type.inputs,
+                rowType.inputs,
                 parsedQuery.elems,
-                row.type.where_clause,
+                rowType.where_clause,
                 null,
+                // @ts-expect-error
                 mgens => {
                     return unifyFunctionTypes(
-                        row.type.output,
+                        rowType.output,
                         parsedQuery.returned,
-                        row.type.where_clause,
+                        rowType.where_clause,
                         mgens,
                         checkTypeMgensForConflict,
                         0, // unboxing depth
@@ -3914,15 +4455,16 @@ class DocSearch {
                 return;
             }
 
+            // @ts-expect-error
             results.max_dist = Math.max(results.max_dist || 0, tfpDist);
-            addIntoResults(results, row.id, pos, 0, tfpDist, 0, Number.MAX_VALUE);
+            addIntoResults(results, row.id.toString(), pos, 0, tfpDist, 0, Number.MAX_VALUE);
         }
 
         /**
          * Compare the query fingerprint with the function fingerprint.
          *
-         * @param {{number}} fullId - The function
-         * @param {{Uint32Array}} queryFingerprint - The query
+         * @param {number} fullId - The function
+         * @param {Uint32Array} queryFingerprint - The query
          * @returns {number|null} - Null if non-match, number if distance
          *                          This function might return 0!
          */
@@ -3953,138 +4495,10 @@ class DocSearch {
 
 
         const innerRunQuery = () => {
-            const queryLen =
-                parsedQuery.elems.reduce((acc, next) => acc + next.pathLast.length, 0) +
-                parsedQuery.returned.reduce((acc, next) => acc + next.pathLast.length, 0);
-            const maxEditDistance = Math.floor(queryLen / 3);
-
-            /**
-             * @type {Map<string, integer>}
-             */
-            const genericSymbols = new Map();
-
-            /**
-             * Convert names to ids in parsed query elements.
-             * This is not used for the "In Names" tab, but is used for the
-             * "In Params", "In Returns", and "In Function Signature" tabs.
-             *
-             * If there is no matching item, but a close-enough match, this
-             * function also that correction.
-             *
-             * See `buildTypeMapIndex` for more information.
-             *
-             * @param {QueryElement} elem
-             * @param {boolean} isAssocType
-             */
-            const convertNameToId = (elem, isAssocType) => {
-                const loweredName = elem.pathLast.toLowerCase();
-                if (this.typeNameIdMap.has(loweredName) &&
-                    (isAssocType || !this.typeNameIdMap.get(loweredName).assocOnly)) {
-                    elem.id = this.typeNameIdMap.get(loweredName).id;
-                } else if (!parsedQuery.literalSearch) {
-                    let match = null;
-                    let matchDist = maxEditDistance + 1;
-                    let matchName = "";
-                    for (const [name, { id, assocOnly }] of this.typeNameIdMap) {
-                        const dist = Math.min(
-                            editDistance(name, loweredName, maxEditDistance),
-                            editDistance(name, elem.normalizedPathLast, maxEditDistance),
-                        );
-                        if (dist <= matchDist && dist <= maxEditDistance &&
-                            (isAssocType || !assocOnly)) {
-                            if (dist === matchDist && matchName > name) {
-                                continue;
-                            }
-                            match = id;
-                            matchDist = dist;
-                            matchName = name;
-                        }
-                    }
-                    if (match !== null) {
-                        parsedQuery.correction = matchName;
-                    }
-                    elem.id = match;
-                }
-                if ((elem.id === null && parsedQuery.totalElems > 1 && elem.typeFilter === -1
-                    && elem.generics.length === 0 && elem.bindings.size === 0)
-                    || elem.typeFilter === TY_GENERIC) {
-                    if (genericSymbols.has(elem.normalizedPathLast)) {
-                        elem.id = genericSymbols.get(elem.normalizedPathLast);
-                    } else {
-                        elem.id = -(genericSymbols.size + 1);
-                        genericSymbols.set(elem.normalizedPathLast, elem.id);
-                    }
-                    if (elem.typeFilter === -1 && elem.normalizedPathLast.length >= 3) {
-                        // Silly heuristic to catch if the user probably meant
-                        // to not write a generic parameter. We don't use it,
-                        // just bring it up.
-                        const maxPartDistance = Math.floor(elem.normalizedPathLast.length / 3);
-                        let matchDist = maxPartDistance + 1;
-                        let matchName = "";
-                        for (const name of this.typeNameIdMap.keys()) {
-                            const dist = editDistance(
-                                name,
-                                elem.normalizedPathLast,
-                                maxPartDistance,
-                            );
-                            if (dist <= matchDist && dist <= maxPartDistance) {
-                                if (dist === matchDist && matchName > name) {
-                                    continue;
-                                }
-                                matchDist = dist;
-                                matchName = name;
-                            }
-                        }
-                        if (matchName !== "") {
-                            parsedQuery.proposeCorrectionFrom = elem.name;
-                            parsedQuery.proposeCorrectionTo = matchName;
-                        }
-                    }
-                    elem.typeFilter = TY_GENERIC;
-                }
-                if (elem.generics.length > 0 && elem.typeFilter === TY_GENERIC) {
-                    // Rust does not have HKT
-                    parsedQuery.error = [
-                        "Generic type parameter ",
-                        elem.name,
-                        " does not accept generic parameters",
-                    ];
-                }
-                for (const elem2 of elem.generics) {
-                    convertNameToId(elem2);
-                }
-                elem.bindings = new Map(Array.from(elem.bindings.entries())
-                    .map(entry => {
-                        const [name, constraints] = entry;
-                        if (!this.typeNameIdMap.has(name)) {
-                            parsedQuery.error = [
-                                "Type parameter ",
-                                name,
-                                " does not exist",
-                            ];
-                            return [null, []];
-                        }
-                        for (const elem2 of constraints) {
-                            convertNameToId(elem2);
-                        }
-
-                        return [this.typeNameIdMap.get(name).id, constraints];
-                    }),
-                );
-            };
-
-            for (const elem of parsedQuery.elems) {
-                convertNameToId(elem);
-                this.buildFunctionTypeFingerprint(elem, parsedQuery.typeFingerprint);
-            }
-            for (const elem of parsedQuery.returned) {
-                convertNameToId(elem);
-                this.buildFunctionTypeFingerprint(elem, parsedQuery.typeFingerprint);
-            }
-
             if (parsedQuery.foundElems === 1 && !parsedQuery.hasReturnArrow) {
                 const elem = parsedQuery.elems[0];
                 // use arrow functions to preserve `this`.
+                // @ts-expect-error
                 const handleNameSearch = id => {
                     const row = this.searchIndex[id];
                     if (!typePassesFilter(elem.typeFilter, row.ty) ||
@@ -4094,6 +4508,7 @@ class DocSearch {
 
                     let pathDist = 0;
                     if (elem.fullPath.length > 1) {
+                        // @ts-expect-error
                         pathDist = checkPath(elem.pathWithoutLast, row);
                         if (pathDist === null) {
                             return;
@@ -4102,11 +4517,13 @@ class DocSearch {
 
                     if (parsedQuery.literalSearch) {
                         if (row.word === elem.pathLast) {
+                            // @ts-expect-error
                             addIntoResults(results_others, row.id, id, 0, 0, pathDist);
                         }
                     } else {
                         addIntoResults(
                             results_others,
+                            // @ts-expect-error
                             row.id,
                             id,
                             row.normalizedName.indexOf(elem.normalizedPathLast),
@@ -4147,23 +4564,31 @@ class DocSearch {
                         const returned = row.type && row.type.output
                             && checkIfInList(row.type.output, elem, row.type.where_clause, null, 0);
                         if (in_args) {
+                            // @ts-expect-error
                             results_in_args.max_dist = Math.max(
+                                // @ts-expect-error
                                 results_in_args.max_dist || 0,
                                 tfpDist,
                             );
                             const maxDist = results_in_args.size < MAX_RESULTS ?
                                 (tfpDist + 1) :
+                                // @ts-expect-error
                                 results_in_args.max_dist;
+                            // @ts-expect-error
                             addIntoResults(results_in_args, row.id, i, -1, tfpDist, 0, maxDist);
                         }
                         if (returned) {
+                            // @ts-expect-error
                             results_returned.max_dist = Math.max(
+                                // @ts-expect-error
                                 results_returned.max_dist || 0,
                                 tfpDist,
                             );
                             const maxDist = results_returned.size < MAX_RESULTS ?
                                 (tfpDist + 1) :
+                                // @ts-expect-error
                                 results_returned.max_dist;
+                            // @ts-expect-error
                             addIntoResults(results_returned, row.id, i, -1, tfpDist, 0, maxDist);
                         }
                     }
@@ -4173,14 +4598,17 @@ class DocSearch {
                 // types with generic parameters go last.
                 // That's because of the way unification is structured: it eats off
                 // the end, and hits a fast path if the last item is a simple atom.
+                // @ts-expect-error
                 const sortQ = (a, b) => {
                     const ag = a.generics.length === 0 && a.bindings.size === 0;
                     const bg = b.generics.length === 0 && b.bindings.size === 0;
                     if (ag !== bg) {
+                        // @ts-expect-error
                         return ag - bg;
                     }
                     const ai = a.id > 0;
                     const bi = b.id > 0;
+                    // @ts-expect-error
                     return ai - bi;
                 };
                 parsedQuery.elems.sort(sortQ);
@@ -4197,8 +4625,11 @@ class DocSearch {
 
         const isType = parsedQuery.foundElems !== 1 || parsedQuery.hasReturnArrow;
         const [sorted_in_args, sorted_returned, sorted_others] = await Promise.all([
+            // @ts-expect-error
             sortResults(results_in_args, "elems", currentCrate),
+            // @ts-expect-error
             sortResults(results_returned, "returned", currentCrate),
+            // @ts-expect-error
             sortResults(results_others, (isType ? "query" : null), currentCrate),
         ]);
         const ret = createQueryResults(
@@ -4210,11 +4641,14 @@ class DocSearch {
             filterCrates, currentCrate);
         await Promise.all([ret.others, ret.returned, ret.in_args].map(async list => {
             const descs = await Promise.all(list.map(result => {
+                // @ts-expect-error
                 return this.searchIndexEmptyDesc.get(result.crate).contains(result.bitIndex) ?
                     "" :
+                    // @ts-expect-error
                     this.searchState.loadDesc(result);
             }));
             for (const [i, result] of list.entries()) {
+                // @ts-expect-error
                 result.desc = descs[i];
             }
         }));
@@ -4229,7 +4663,9 @@ class DocSearch {
 
 // ==================== Core search logic end ====================
 
+/** @type {Map<string, rustdoc.RawSearchIndexCrate>} */
 let rawSearchIndex;
+// @ts-expect-error
 let docSearch;
 const longItemTypes = [
     "keyword",
@@ -4259,13 +4695,16 @@ const longItemTypes = [
     "derive macro",
     "trait alias",
 ];
+// @ts-expect-error
 let currentResults;
 
 // In the search display, allows to switch between tabs.
+// @ts-expect-error
 function printTab(nb) {
     let iter = 0;
     let foundCurrentTab = false;
     let foundCurrentResultSet = false;
+    // @ts-expect-error
     onEachLazy(document.getElementById("search-tabs").childNodes, elem => {
         if (nb === iter) {
             addClass(elem, "selected");
@@ -4277,6 +4716,7 @@ function printTab(nb) {
     });
     const isTypeSearch = (nb > 0 || iter === 1);
     iter = 0;
+    // @ts-expect-error
     onEachLazy(document.getElementById("results").childNodes, elem => {
         if (nb === iter) {
             addClass(elem, "active");
@@ -4287,12 +4727,15 @@ function printTab(nb) {
         iter += 1;
     });
     if (foundCurrentTab && foundCurrentResultSet) {
+        // @ts-expect-error
         searchState.currentTab = nb;
         // Corrections only kick in on type-based searches.
         const correctionsElem = document.getElementsByClassName("search-corrections");
         if (isTypeSearch) {
+            // @ts-expect-error
             removeClass(correctionsElem[0], "hidden");
         } else {
+            // @ts-expect-error
             addClass(correctionsElem[0], "hidden");
         }
     } else if (nb !== 0) {
@@ -4326,16 +4769,22 @@ function getFilterCrates() {
     const elem = document.getElementById("crate-search");
 
     if (elem &&
+        // @ts-expect-error
         elem.value !== "all crates" &&
+        // @ts-expect-error
         window.searchIndex.has(elem.value)
     ) {
+        // @ts-expect-error
         return elem.value;
     }
     return null;
 }
 
+// @ts-expect-error
 function nextTab(direction) {
+    // @ts-expect-error
     const next = (searchState.currentTab + direction + 3) % searchState.focusedByTab.length;
+    // @ts-expect-error
     searchState.focusedByTab[searchState.currentTab] = document.activeElement;
     printTab(next);
     focusSearchResult();
@@ -4344,9 +4793,12 @@ function nextTab(direction) {
 // Focus the first search result on the active tab, or the result that
 // was focused last time this tab was active.
 function focusSearchResult() {
+    // @ts-expect-error
     const target = searchState.focusedByTab[searchState.currentTab] ||
         document.querySelectorAll(".search-results.active a").item(0) ||
+        // @ts-expect-error
         document.querySelectorAll("#search-tabs button").item(searchState.currentTab);
+    // @ts-expect-error
     searchState.focusedByTab[searchState.currentTab] = null;
     if (target) {
         target.focus();
@@ -4356,9 +4808,8 @@ function focusSearchResult() {
 /**
  * Render a set of search results for a single tab.
  * @param {Array<?>}    array   - The search results for this tab
- * @param {ParsedQuery} query
+ * @param {rustdoc.ParsedQuery<rustdoc.QueryElement>} query
  * @param {boolean}     display - True if this is the active tab
- * @param {"sig"|"elems"|"returned"|null} typeInfo
  */
 async function addTab(array, query, display) {
     const extraClass = display ? " active" : "";
@@ -4405,6 +4856,7 @@ ${item.displayPath}<span class="${type}">${name}</span>\
             if (item.displayTypeSignature) {
                 const {type, mappedNames, whereClause} = await item.displayTypeSignature;
                 const displayType = document.createElement("div");
+                // @ts-expect-error
                 type.forEach((value, index) => {
                     if (index % 2 !== 0) {
                         const highlight = document.createElement("strong");
@@ -4445,6 +4897,7 @@ ${item.displayPath}<span class="${type}">${name}</span>\
                         const line = document.createElement("div");
                         line.className = "where";
                         line.appendChild(document.createTextNode(`    ${name}: `));
+                        // @ts-expect-error
                         innerType.forEach((value, index) => {
                             if (index % 2 !== 0) {
                                 const highlight = document.createElement("strong");
@@ -4488,6 +4941,7 @@ ${item.displayPath}<span class="${type}">${name}</span>\
     return output;
 }
 
+// @ts-expect-error
 function makeTabHeader(tabNb, text, nbElems) {
     // https://blog.horizon-eda.org/misc/2020/02/19/ui.html
     //
@@ -4497,6 +4951,7 @@ function makeTabHeader(tabNb, text, nbElems) {
     const fmtNbElems =
         nbElems < 10  ? `\u{2007}(${nbElems})\u{2007}\u{2007}` :
         nbElems < 100 ? `\u{2007}(${nbElems})\u{2007}` : `\u{2007}(${nbElems})`;
+    // @ts-expect-error
     if (searchState.currentTab === tabNb) {
         return "<button class=\"selected\">" + text +
             "<span class=\"count\">" + fmtNbElems + "</span></button>";
@@ -4505,11 +4960,12 @@ function makeTabHeader(tabNb, text, nbElems) {
 }
 
 /**
- * @param {ResultsTable} results
+ * @param {rustdoc.ResultsTable} results
  * @param {boolean} go_to_first
  * @param {string} filterCrates
  */
 async function showResults(results, go_to_first, filterCrates) {
+    // @ts-expect-error
     const search = searchState.outputElement();
     if (go_to_first || (results.others.length === 1
         && getSettingValue("go-to-only-result") === "true")
@@ -4527,6 +4983,7 @@ async function showResults(results, go_to_first, filterCrates) {
         // will be used, starting search again since the search input is not empty, leading you
         // back to the previous page again.
         window.onunload = () => { };
+        // @ts-expect-error
         searchState.removeQueryParameters();
         const elem = document.createElement("a");
         elem.href = results.others[0].href;
@@ -4537,6 +4994,7 @@ async function showResults(results, go_to_first, filterCrates) {
         return;
     }
     if (results.query === undefined) {
+        // @ts-expect-error
         results.query = DocSearch.parseQuery(searchState.input.value);
     }
 
@@ -4545,6 +5003,7 @@ async function showResults(results, go_to_first, filterCrates) {
     // Navigate to the relevant tab if the current tab is empty, like in case users search
     // for "-> String". If they had selected another tab previously, they have to click on
     // it again.
+    // @ts-expect-error
     let currentTab = searchState.currentTab;
     if ((currentTab === 0 && results.others.length === 0) ||
         (currentTab === 1 && results.in_args.length === 0) ||
@@ -4572,6 +5031,7 @@ async function showResults(results, go_to_first, filterCrates) {
         <h1 class="search-results-title">Results</h1>${crates}</div>`;
     if (results.query.error !== null) {
         const error = results.query.error;
+        // @ts-expect-error
         error.forEach((value, index) => {
             value = value.split("<").join("&lt;").split(">").join("&gt;");
             if (index % 2 !== 0) {
@@ -4632,7 +5092,9 @@ async function showResults(results, go_to_first, filterCrates) {
     resultsElem.appendChild(ret_returned);
 
     search.innerHTML = output;
+    // @ts-expect-error
     if (searchState.rustdocToolbar) {
+        // @ts-expect-error
         search.querySelector(".main-heading").appendChild(searchState.rustdocToolbar);
     }
     const crateSearch = document.getElementById("crate-search");
@@ -4641,23 +5103,30 @@ async function showResults(results, go_to_first, filterCrates) {
     }
     search.appendChild(resultsElem);
     // Reset focused elements.
+    // @ts-expect-error
     searchState.showResults(search);
+    // @ts-expect-error
     const elems = document.getElementById("search-tabs").childNodes;
+    // @ts-expect-error
     searchState.focusedByTab = [];
     let i = 0;
     for (const elem of elems) {
         const j = i;
+        // @ts-expect-error
         elem.onclick = () => printTab(j);
+        // @ts-expect-error
         searchState.focusedByTab.push(null);
         i += 1;
     }
     printTab(currentTab);
 }
 
+// @ts-expect-error
 function updateSearchHistory(url) {
     if (!browserSupportsHistoryApi()) {
         return;
     }
+    // @ts-expect-error
     const params = searchState.getQueryStringParams();
     if (!history.state && !params.search) {
         history.pushState(null, "", url);
@@ -4672,9 +5141,11 @@ function updateSearchHistory(url) {
  * @param {boolean} [forced]
  */
 async function search(forced) {
+    // @ts-expect-error
     const query = DocSearch.parseQuery(searchState.input.value.trim());
     let filterCrates = getFilterCrates();
 
+    // @ts-expect-error
     if (!forced && query.userQuery === currentResults) {
         if (query.userQuery.length > 0) {
             putBackSearch();
@@ -4682,8 +5153,10 @@ async function search(forced) {
         return;
     }
 
+    // @ts-expect-error
     searchState.setLoadingSearch();
 
+    // @ts-expect-error
     const params = searchState.getQueryStringParams();
 
     // In case we have no information about the saved crate and there is a URL query parameter,
@@ -4693,6 +5166,7 @@ async function search(forced) {
     }
 
     // Update document title to maintain a meaningful browser history
+    // @ts-expect-error
     searchState.title = "\"" + query.userQuery + "\" Search - Rust";
 
     // Because searching is incremental by character, only the most
@@ -4700,8 +5174,10 @@ async function search(forced) {
     updateSearchHistory(buildUrl(query.userQuery, filterCrates));
 
     await showResults(
+        // @ts-expect-error
         await docSearch.execQuery(query, filterCrates, window.currentCrate),
         params.go_to_first,
+        // @ts-expect-error
         filterCrates);
 }
 
@@ -4710,62 +5186,83 @@ async function search(forced) {
  * @param {Event} [e] - The event that triggered this call, if any
  */
 function onSearchSubmit(e) {
+    // @ts-expect-error
     e.preventDefault();
+    // @ts-expect-error
     searchState.clearInputTimeout();
     search();
 }
 
 function putBackSearch() {
+    // @ts-expect-error
     const search_input = searchState.input;
+    // @ts-expect-error
     if (!searchState.input) {
         return;
     }
+    // @ts-expect-error
     if (search_input.value !== "" && !searchState.isDisplayed()) {
+        // @ts-expect-error
         searchState.showResults();
         if (browserSupportsHistoryApi()) {
             history.replaceState(null, "",
                 buildUrl(search_input.value, getFilterCrates()));
         }
+        // @ts-expect-error
         document.title = searchState.title;
     }
 }
 
 function registerSearchEvents() {
+    // @ts-expect-error
     const params = searchState.getQueryStringParams();
 
     // Populate search bar with query string search term when provided,
     // but only if the input bar is empty. This avoid the obnoxious issue
     // where you start trying to do a search, and the index loads, and
     // suddenly your search is gone!
+    // @ts-expect-error
     if (searchState.input.value === "") {
+        // @ts-expect-error
         searchState.input.value = params.search || "";
     }
 
     const searchAfter500ms = () => {
+        // @ts-expect-error
         searchState.clearInputTimeout();
+        // @ts-expect-error
         if (searchState.input.value.length === 0) {
+            // @ts-expect-error
             searchState.hideResults();
         } else {
+            // @ts-expect-error
             searchState.timeout = setTimeout(search, 500);
         }
     };
+    // @ts-expect-error
     searchState.input.onkeyup = searchAfter500ms;
+    // @ts-expect-error
     searchState.input.oninput = searchAfter500ms;
+    // @ts-expect-error
     document.getElementsByClassName("search-form")[0].onsubmit = onSearchSubmit;
+    // @ts-expect-error
     searchState.input.onchange = e => {
         if (e.target !== document.activeElement) {
             // To prevent doing anything when it's from a blur event.
             return;
         }
         // Do NOT e.preventDefault() here. It will prevent pasting.
+        // @ts-expect-error
         searchState.clearInputTimeout();
         // zero-timeout necessary here because at the time of event handler execution the
         // pasted content is not in the input field yet. Shouldn’t make any difference for
         // change, though.
         setTimeout(search, 0);
     };
+    // @ts-expect-error
     searchState.input.onpaste = searchState.input.onchange;
 
+    // @ts-expect-error
     searchState.outputElement().addEventListener("keydown", e => {
         // We only handle unmodified keystrokes here. We don't want to interfere with,
         // for instance, alt-left and alt-right for history navigation.
@@ -4775,18 +5272,24 @@ function registerSearchEvents() {
         // up and down arrow select next/previous search result, or the
         // search box if we're already at the top.
         if (e.which === 38) { // up
+            // @ts-expect-error
             const previous = document.activeElement.previousElementSibling;
             if (previous) {
+                // @ts-expect-error
                 previous.focus();
             } else {
+                // @ts-expect-error
                 searchState.focus();
             }
             e.preventDefault();
         } else if (e.which === 40) { // down
+            // @ts-expect-error
             const next = document.activeElement.nextElementSibling;
             if (next) {
+                // @ts-expect-error
                 next.focus();
             }
+            // @ts-expect-error
             const rect = document.activeElement.getBoundingClientRect();
             if (window.innerHeight - rect.bottom < rect.height) {
                 window.scrollBy(0, rect.height);
@@ -4801,6 +5304,7 @@ function registerSearchEvents() {
         }
     });
 
+    // @ts-expect-error
     searchState.input.addEventListener("keydown", e => {
         if (e.which === 40) { // down
             focusSearchResult();
@@ -4808,11 +5312,14 @@ function registerSearchEvents() {
         }
     });
 
+    // @ts-expect-error
     searchState.input.addEventListener("focus", () => {
         putBackSearch();
     });
 
+    // @ts-expect-error
     searchState.input.addEventListener("blur", () => {
+        // @ts-expect-error
         searchState.input.placeholder = searchState.input.origPlaceholder;
     });
 
@@ -4823,6 +5330,7 @@ function registerSearchEvents() {
         const previousTitle = document.title;
 
         window.addEventListener("popstate", e => {
+            // @ts-expect-error
             const params = searchState.getQueryStringParams();
             // Revert to the previous title manually since the History
             // API ignores the title parameter.
@@ -4836,6 +5344,7 @@ function registerSearchEvents() {
             // nothing there, which lets you really go back to a
             // previous state with nothing in the bar.
             if (params.search && params.search.length > 0) {
+                // @ts-expect-error
                 searchState.input.value = params.search;
                 // Some browsers fire "onpopstate" for every page load
                 // (Chrome), while others fire the event only when actually
@@ -4845,9 +5354,11 @@ function registerSearchEvents() {
                 e.preventDefault();
                 search();
             } else {
+                // @ts-expect-error
                 searchState.input.value = "";
                 // When browsing back from search results the main page
                 // visibility must be reset.
+                // @ts-expect-error
                 searchState.hideResults();
             }
         });
@@ -4860,17 +5371,22 @@ function registerSearchEvents() {
     // that try to sync state between the URL and the search input. To work around it,
     // do a small amount of re-init on page show.
     window.onpageshow = () => {
+        // @ts-expect-error
         const qSearch = searchState.getQueryStringParams().search;
+        // @ts-expect-error
         if (searchState.input.value === "" && qSearch) {
+            // @ts-expect-error
             searchState.input.value = qSearch;
         }
         search();
     };
 }
 
+// @ts-expect-error
 function updateCrate(ev) {
     if (ev.target.value === "all crates") {
         // If we don't remove it from the URL, it'll be picked up again by the search.
+        // @ts-expect-error
         const query = searchState.input.value.trim();
         updateSearchHistory(buildUrl(query, null));
     }
@@ -4881,9 +5397,11 @@ function updateCrate(ev) {
     search(true);
 }
 
+// @ts-expect-error
 function initSearch(searchIndx) {
     rawSearchIndex = searchIndx;
     if (typeof window !== "undefined") {
+        // @ts-expect-error
         docSearch = new DocSearch(rawSearchIndex, ROOT_PATH, searchState);
         registerSearchEvents();
         // If there's a search term in the URL, execute the search now.
@@ -4891,6 +5409,7 @@ function initSearch(searchIndx) {
             search();
         }
     } else if (typeof exports !== "undefined") {
+        // @ts-expect-error
         docSearch = new DocSearch(rawSearchIndex, ROOT_PATH, searchState);
         exports.docSearch = docSearch;
         exports.parseQuery = DocSearch.parseQuery;
@@ -4902,8 +5421,11 @@ if (typeof exports !== "undefined") {
 }
 
 if (typeof window !== "undefined") {
+    // @ts-expect-error
     window.initSearch = initSearch;
+    // @ts-expect-error
     if (window.searchIndex !== undefined) {
+        // @ts-expect-error
         initSearch(window.searchIndex);
     }
 } else {
@@ -4918,19 +5440,23 @@ if (typeof window !== "undefined") {
 // https://fossies.org/linux/lucene/lucene/core/src/java/org/apache/lucene/util/automaton/
 //   LevenshteinAutomata.java
 class ParametricDescription {
+    // @ts-expect-error
     constructor(w, n, minErrors) {
         this.w = w;
         this.n = n;
         this.minErrors = minErrors;
     }
+    // @ts-expect-error
     isAccept(absState) {
         const state = Math.floor(absState / (this.w + 1));
         const offset = absState % (this.w + 1);
         return this.w - offset + this.minErrors[state] <= this.n;
     }
+    // @ts-expect-error
     getPosition(absState) {
         return absState % (this.w + 1);
     }
+    // @ts-expect-error
     getVector(name, charCode, pos, end) {
         let vector = 0;
         for (let i = pos; i < end; i += 1) {
@@ -4941,6 +5467,7 @@ class ParametricDescription {
         }
         return vector;
     }
+    // @ts-expect-error
     unpack(data, index, bitsPerValue) {
         const bitLoc = (bitsPerValue * index);
         const dataLoc = bitLoc >> 5;
diff --git a/src/librustdoc/html/static/js/settings.js b/src/librustdoc/html/static/js/settings.js
index 183663b94fc28..d7b0e4b4f541e 100644
--- a/src/librustdoc/html/static/js/settings.js
+++ b/src/librustdoc/html/static/js/settings.js
@@ -3,6 +3,9 @@
 /* global addClass, removeClass, onEach, onEachLazy */
 /* global MAIN_ID, getVar, getSettingsButton, getHelpButton */
 
+// Eventually fix this.
+// @ts-nocheck
+
 "use strict";
 
 (function() {
diff --git a/src/librustdoc/html/static/js/src-script.js b/src/librustdoc/html/static/js/src-script.js
index 3003f4c150338..8f712f4c20c7b 100644
--- a/src/librustdoc/html/static/js/src-script.js
+++ b/src/librustdoc/html/static/js/src-script.js
@@ -5,6 +5,9 @@
 /* global addClass, onEachLazy, removeClass, browserSupportsHistoryApi */
 /* global updateLocalStorage, getVar */
 
+// Eventually fix this.
+// @ts-nocheck
+
 "use strict";
 
 (function() {
diff --git a/src/librustdoc/html/static/js/storage.js b/src/librustdoc/html/static/js/storage.js
index d77804d045e36..4770ccc127993 100644
--- a/src/librustdoc/html/static/js/storage.js
+++ b/src/librustdoc/html/static/js/storage.js
@@ -5,15 +5,28 @@
 // the page, so we don't see major layout changes during the load of the page.
 "use strict";
 
+/**
+ * @import * as rustdoc from "./rustdoc.d.ts";
+ */
+
 const builtinThemes = ["light", "dark", "ayu"];
 const darkThemes = ["dark", "ayu"];
-window.currentTheme = document.getElementById("themeStyle");
+window.currentTheme = (function() {
+    const currentTheme = document.getElementById("themeStyle");
+    return currentTheme instanceof HTMLLinkElement ? currentTheme : null;
+})();
 
 const settingsDataset = (function() {
     const settingsElement = document.getElementById("default-settings");
     return settingsElement && settingsElement.dataset ? settingsElement.dataset : null;
 })();
 
+/**
+ * Get a configuration value. If it's not set, get the default.
+ *
+ * @param {string} settingName
+ * @returns
+ */
 function getSettingValue(settingName) {
     const current = getCurrentValue(settingName);
     if (current === null && settingsDataset !== null) {
@@ -29,17 +42,39 @@ function getSettingValue(settingName) {
 
 const localStoredTheme = getSettingValue("theme");
 
+/**
+ * Check if a DOM Element has the given class set.
+ * If `elem` is null, returns false.
+ *
+ * @param {HTMLElement|null} elem
+ * @param {string} className
+ * @returns {boolean}
+ */
 // eslint-disable-next-line no-unused-vars
 function hasClass(elem, className) {
-    return elem && elem.classList && elem.classList.contains(className);
+    return !!elem && !!elem.classList && elem.classList.contains(className);
 }
 
+/**
+ * Add a class to a DOM Element. If `elem` is null,
+ * does nothing. This function is idempotent.
+ *
+ * @param {HTMLElement|null} elem
+ * @param {string} className
+ */
 function addClass(elem, className) {
     if (elem && elem.classList) {
         elem.classList.add(className);
     }
 }
 
+/**
+ * Remove a class from a DOM Element. If `elem` is null,
+ * does nothing. This function is idempotent.
+ *
+ * @param {HTMLElement|null} elem
+ * @param {string} className
+ */
 // eslint-disable-next-line no-unused-vars
 function removeClass(elem, className) {
     if (elem && elem.classList) {
@@ -49,8 +84,8 @@ function removeClass(elem, className) {
 
 /**
  * Run a callback for every element of an Array.
- * @param {Array<?>}    arr        - The array to iterate over
- * @param {function(?)} func       - The callback
+ * @param {Array<?>}                       arr  - The array to iterate over
+ * @param {function(?): boolean|undefined} func - The callback
  */
 function onEach(arr, func) {
     for (const elem of arr) {
@@ -67,8 +102,8 @@ function onEach(arr, func) {
  * or a "live" NodeList while modifying it can be very slow.
  * https://developer.mozilla.org/en-US/docs/Web/API/HTMLCollection
  * https://developer.mozilla.org/en-US/docs/Web/API/NodeList
- * @param {NodeList<?>|HTMLCollection<?>} lazyArray  - An array to iterate over
- * @param {function(?)}                   func       - The callback
+ * @param {NodeList|HTMLCollection} lazyArray  - An array to iterate over
+ * @param {function(?): boolean}    func       - The callback
  */
 // eslint-disable-next-line no-unused-vars
 function onEachLazy(lazyArray, func) {
@@ -77,6 +112,15 @@ function onEachLazy(lazyArray, func) {
         func);
 }
 
+/**
+ * Set a configuration value. This uses localstorage,
+ * with a `rustdoc-` prefix, to avoid clashing with other
+ * web apps that may be running in the same domain (for example, mdBook).
+ * If localStorage is disabled, this function does nothing.
+ *
+ * @param {string} name
+ * @param {string} value
+ */
 function updateLocalStorage(name, value) {
     try {
         window.localStorage.setItem("rustdoc-" + name, value);
@@ -85,6 +129,15 @@ function updateLocalStorage(name, value) {
     }
 }
 
+/**
+ * Get a configuration value. If localStorage is disabled,
+ * this function returns null. If the setting was never
+ * changed by the user, it also returns null; if you want to
+ * be able to use a default value, call `getSettingValue` instead.
+ *
+ * @param {string} name
+ * @returns {string|null}
+ */
 function getCurrentValue(name) {
     try {
         return window.localStorage.getItem("rustdoc-" + name);
@@ -93,19 +146,29 @@ function getCurrentValue(name) {
     }
 }
 
-// Get a value from the rustdoc-vars div, which is used to convey data from
-// Rust to the JS. If there is no such element, return null.
-const getVar = (function getVar(name) {
+/**
+ * Get a value from the rustdoc-vars div, which is used to convey data from
+ * Rust to the JS. If there is no such element, return null.
+ *
+ * @param {string} name
+ * @returns {string|null}
+ */
+function getVar(name) {
     const el = document.querySelector("head > meta[name='rustdoc-vars']");
-    return el ? el.attributes["data-" + name].value : null;
-});
+    return el ? el.getAttribute("data-" + name) : null;
+}
 
+/**
+ * Change the current theme.
+ * @param {string|null} newThemeName
+ * @param {boolean} saveTheme
+ */
 function switchTheme(newThemeName, saveTheme) {
-    const themeNames = getVar("themes").split(",").filter(t => t);
+    const themeNames = (getVar("themes") || "").split(",").filter(t => t);
     themeNames.push(...builtinThemes);
 
     // Ensure that the new theme name is among the defined themes
-    if (themeNames.indexOf(newThemeName) === -1) {
+    if (newThemeName === null || themeNames.indexOf(newThemeName) === -1) {
         return;
     }
 
@@ -118,7 +181,7 @@ function switchTheme(newThemeName, saveTheme) {
     document.documentElement.setAttribute("data-theme", newThemeName);
 
     if (builtinThemes.indexOf(newThemeName) !== -1) {
-        if (window.currentTheme) {
+        if (window.currentTheme && window.currentTheme.parentNode) {
             window.currentTheme.parentNode.removeChild(window.currentTheme);
             window.currentTheme = null;
         }
@@ -130,7 +193,10 @@ function switchTheme(newThemeName, saveTheme) {
             // rendering, but if we are done, it would blank the page.
             if (document.readyState === "loading") {
                 document.write(`<link rel="stylesheet" id="themeStyle" href="${newHref}">`);
-                window.currentTheme = document.getElementById("themeStyle");
+                window.currentTheme = (function() {
+                    const currentTheme = document.getElementById("themeStyle");
+                    return currentTheme instanceof HTMLLinkElement ? currentTheme : null;
+                })();
             } else {
                 window.currentTheme = document.createElement("link");
                 window.currentTheme.rel = "stylesheet";
@@ -179,11 +245,13 @@ const updateTheme = (function() {
     return updateTheme;
 })();
 
+// @ts-ignore
 if (getSettingValue("use-system-theme") !== "false" && window.matchMedia) {
     // update the preferred dark theme if the user is already using a dark theme
     // See https://github.com/rust-lang/rust/pull/77809#issuecomment-707875732
     if (getSettingValue("use-system-theme") === null
         && getSettingValue("preferred-dark-theme") === null
+        && localStoredTheme !== null
         && darkThemes.indexOf(localStoredTheme) >= 0) {
         updateLocalStorage("preferred-dark-theme", localStoredTheme);
     }
diff --git a/src/librustdoc/html/static/js/tsconfig.json b/src/librustdoc/html/static/js/tsconfig.json
new file mode 100644
index 0000000000000..b81099bb9dfd0
--- /dev/null
+++ b/src/librustdoc/html/static/js/tsconfig.json
@@ -0,0 +1,15 @@
+{
+  "compilerOptions": {
+    "target": "es2023",
+    "module": "esnext",
+    "rootDir": "./",
+    "allowJs": true,
+    "checkJs": true,
+    "noEmit": true,
+    "strict": true,
+    "skipLibCheck": true
+  },
+  "typeAcquisition": {
+    "include": ["./rustdoc.d.ts"]
+  }
+}