Convert files to typescript

The change converts the following files to typescript:

* elements/core/gr-router/gr-router.ts

Change-Id: I827da7e6ed664b3d2188a849b863cf53c5844758
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
index 8c0e4d3..74de20d 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -14,16 +14,57 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
-import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {page} from '../../../utils/page-wrapper-utils.js';
-import {htmlTemplate} from './gr-router_html.js';
-import {encodeURL, getBaseUrl} from '../../../utils/url-util.js';
-import {GerritNav} from '../gr-navigation/gr-navigation.js';
-import {appContext} from '../../../services/app-context.js';
-import {patchNumEquals} from '../../../utils/patch-set-util.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
+import {PolymerElement} from '@polymer/polymer/polymer-element';
+import {
+  page,
+  PageContext,
+  PageNextCallback,
+} from '../../../utils/page-wrapper-utils';
+import {htmlTemplate} from './gr-router_html';
+import {encodeURL, getBaseUrl} from '../../../utils/url-util';
+import {
+  DashboardSection,
+  GenerateUrlChangeViewParameters,
+  GenerateUrlDashboardViewParameters,
+  GenerateUrlDiffViewParameters,
+  GenerateUrlEditViewParameters,
+  GenerateUrlGroupViewParameters,
+  GenerateUrlParameters,
+  GenerateUrlRepoViewParameters,
+  GenerateUrlSearchViewParameters,
+  GenerateWebLinksChangeParameters,
+  GenerateWebLinksFileParameters,
+  GenerateWebLinksParameters,
+  GenerateWebLinksPatchsetParameters,
+  GerritView,
+  isGenerateUrlDiffViewParameters,
+  RepoDetailView,
+  WeblinkType,
+  GroupDetailView,
+  GerritNav,
+  GeneratedWebLink,
+} from '../gr-navigation/gr-navigation';
+import {appContext} from '../../../services/app-context';
+import {patchNumEquals} from '../../../utils/patch-set-util';
+import {customElement, property} from '@polymer/decorators';
+import {assertNever} from '../../../utils/common-util';
+import {
+  GroupId,
+  NumericChangeId,
+  PatchSetNum,
+  RepoName,
+  ServerInfo,
+  UrlEncodedCommentId,
+} from '../../../types/common';
+import {GrRestApiInterface} from '../../shared/gr-rest-api-interface/gr-rest-api-interface';
+import {
+  AppElement,
+  AppElementParams,
+  AppElementAgreementParam,
+} from '../../gr-app-types';
 
 const RoutePattern = {
   ROOT: '/',
@@ -93,13 +134,12 @@
   BRANCH_LIST_OFFSET: /^\/admin\/repos\/(.+),branches(,(.+))?$/,
   BRANCH_LIST_FILTER: '/admin/repos/:repo,branches/q/filter::filter',
   BRANCH_LIST_FILTER_OFFSET:
-      '/admin/repos/:repo,branches/q/filter::filter,:offset',
+    '/admin/repos/:repo,branches/q/filter::filter,:offset',
 
   // Matches /admin/repos/<repo>,tags[,<offset>].
   TAG_LIST_OFFSET: /^\/admin\/repos\/(.+),tags(,(.+))?$/,
   TAG_LIST_FILTER: '/admin/repos/:repo,tags/q/filter::filter',
-  TAG_LIST_FILTER_OFFSET:
-      '/admin/repos/:repo,tags/q/filter::filter,:offset',
+  TAG_LIST_FILTER_OFFSET: '/admin/repos/:repo,tags/q/filter::filter,:offset',
 
   PLUGINS: /^\/plugins\/(.+)$/,
 
@@ -213,113 +253,148 @@
 }
 
 // Setup listeners outside of the router component initialization.
-(function() {
+(function () {
   window.addEventListener('WebComponentsReady', () => {
     appContext.reportingService.timeEnd('WebComponentsReady');
   });
 })();
 
-/**
- * @extends PolymerElement
- */
-class GrRouter extends GestureEventListeners(
-    LegacyElementMixin(PolymerElement)) {
-  static get template() { return htmlTemplate; }
+export interface GrRouter {
+  $: {
+    restAPI: GrRestApiInterface;
+  };
+}
 
-  static get is() { return 'gr-router'; }
+export interface PageContextWithQueryMap extends PageContext {
+  queryMap: Map<string, string> | URLSearchParams;
+}
 
-  static get properties() {
-    return {
-      _app: {
-        type: Object,
-        value: app,
-      },
-      _isRedirecting: Boolean,
-      // This variable is to differentiate between internal navigation (false)
-      // and for first navigation in app after loaded from server (true).
-      _isInitialLoad: {
-        type: Boolean,
-        value: true,
-      },
-    };
+type QueryStringItem = [string, string]; // [key, value]
+
+type GenerateUrlLegacyChangeViewParameters = Omit<
+  GenerateUrlChangeViewParameters,
+  'project'
+>;
+type GenerateUrlLegacyDiffViewParameters = Omit<
+  GenerateUrlDiffViewParameters,
+  'project'
+>;
+
+interface PatchRangeParams {
+  patchNum?: PatchSetNum | null;
+  basePatchNum?: PatchSetNum | null;
+}
+
+@customElement('gr-router')
+export class GrRouter extends GestureEventListeners(
+  LegacyElementMixin(PolymerElement)
+) {
+  static get template() {
+    return htmlTemplate;
   }
 
+  @property({type: Object})
+  readonly _app = app;
+
+  @property({type: Boolean})
+  _isRedirecting?: boolean;
+
+  // This variable is to differentiate between internal navigation (false)
+  // and for first navigation in app after loaded from server (true).
+  @property({type: Boolean})
+  _isInitialLoad = true;
+
+  private readonly reporting = appContext.reportingService;
+
   constructor() {
     super();
-    this.reporting = appContext.reportingService;
   }
 
   start() {
-    if (!this._app) { return; }
+    if (!this._app) {
+      return;
+    }
     this._startRouter();
   }
 
-  _setParams(params) {
+  _setParams(params: AppElementParams | GenerateUrlParameters) {
     this._appElement().params = params;
   }
 
-  _appElement() {
+  _appElement(): AppElement {
     // In Polymer2 you have to reach through the shadow root of the app
     // element. This obviously breaks encapsulation.
     // TODO(brohlfs): Make this more elegant, e.g. by exposing app-element
     // explicitly in app, or by delegating to it.
-    return document.getElementById('app-element') ||
-        document.getElementById('app').shadowRoot.getElementById(
-            'app-element');
+
+    // It is expected that application has a GrAppElement(id=='app-element')
+    // at the document level or inside the shadow root of the GrApp (id='app')
+    // element.
+    return (document.getElementById('app-element') ||
+      document
+        .getElementById('app')!
+        .shadowRoot!.getElementById('app-element')!) as AppElement;
   }
 
-  _redirect(url) {
+  _redirect(url: string) {
     this._isRedirecting = true;
     page.redirect(url);
   }
 
   /**
-   * @param {!Object} params
-   * @return {string}
+   * @param params
+   * @return
    */
-  _generateUrl(params) {
+  _generateUrl(params: GenerateUrlParameters) {
     const base = getBaseUrl();
     let url = '';
-    const Views = GerritNav.View;
 
-    if (params.view === Views.SEARCH) {
+    if (params.view === GerritView.SEARCH) {
       url = this._generateSearchUrl(params);
-    } else if (params.view === Views.CHANGE) {
+    } else if (params.view === GerritView.CHANGE) {
       url = this._generateChangeUrl(params);
-    } else if (params.view === Views.DASHBOARD) {
+    } else if (params.view === GerritView.DASHBOARD) {
       url = this._generateDashboardUrl(params);
-    } else if (params.view === Views.DIFF || params.view === Views.EDIT) {
+    } else if (
+      params.view === GerritView.DIFF ||
+      params.view === GerritView.EDIT
+    ) {
       url = this._generateDiffOrEditUrl(params);
-    } else if (params.view === Views.GROUP) {
+    } else if (params.view === GerritView.GROUP) {
       url = this._generateGroupUrl(params);
-    } else if (params.view === Views.REPO) {
+    } else if (params.view === GerritView.REPO) {
       url = this._generateRepoUrl(params);
-    } else if (params.view === Views.ROOT) {
+    } else if (params.view === GerritView.ROOT) {
       url = '/';
-    } else if (params.view === Views.SETTINGS) {
-      url = this._generateSettingsUrl(params);
+    } else if (params.view === GerritView.SETTINGS) {
+      url = this._generateSettingsUrl();
     } else {
-      throw new Error('Can\'t generate');
+      assertNever(params, "Can't generate");
     }
 
     return base + url;
   }
 
-  _generateWeblinks(params) {
-    const type = params.type;
-    switch (type) {
-      case GerritNav.WeblinkType.FILE:
+  _generateWeblinks(
+    params: GenerateWebLinksParameters
+  ): GeneratedWebLink[] | GeneratedWebLink {
+    switch (params.type) {
+      case WeblinkType.FILE:
         return this._getFileWebLinks(params);
-      case GerritNav.WeblinkType.CHANGE:
+      case WeblinkType.CHANGE:
         return this._getChangeWeblinks(params);
-      case GerritNav.WeblinkType.PATCHSET:
+      case WeblinkType.PATCHSET:
         return this._getPatchSetWeblink(params);
       default:
-        console.warn(`Unsupported weblink ${type}!`);
+        console.warn(`Unsupported weblink ${(params as any).type}!`);
+        // TODO(TS): use assertNever(params.type)
+        return [];
     }
   }
 
-  _getPatchSetWeblink(params) {
+  _getPatchSetWeblink(
+    params: GenerateWebLinksPatchsetParameters
+  ): GeneratedWebLink {
     const {commit, options} = params;
     const {weblinks, config} = options || {};
     const name = commit && commit.slice(0, 7);
@@ -331,62 +406,70 @@
     }
   }
 
-  _firstCodeBrowserWeblink(weblinks) {
+  _firstCodeBrowserWeblink(weblinks: GeneratedWebLink[]) {
     // This is an ordered allowed list of web link types that provide direct
     // links to the commit in the url property.
     const codeBrowserLinks = ['gitiles', 'browse', 'gitweb'];
     for (let i = 0; i < codeBrowserLinks.length; i++) {
-      const weblink =
-        weblinks.find(weblink => weblink.name === codeBrowserLinks[i]);
-      if (weblink) { return weblink; }
+      const weblink = weblinks.find(
+        weblink => weblink.name === codeBrowserLinks[i]
+      );
+      if (weblink) {
+        return weblink;
+      }
     }
     return null;
   }
 
-  _getBrowseCommitWeblink(weblinks, config) {
-    if (!weblinks) { return null; }
+  _getBrowseCommitWeblink(weblinks?: GeneratedWebLink[], config?: ServerInfo) {
+    if (!weblinks) {
+      return null;
+    }
     let weblink;
     // Use primary weblink if configured and exists.
     if (config && config.gerrit && config.gerrit.primary_weblink_name) {
-      weblink = weblinks.find(
-          weblink => weblink.name === config.gerrit.primary_weblink_name
-      );
+      const primaryWeblinkName = config.gerrit.primary_weblink_name;
+      weblink = weblinks.find(weblink => weblink.name === primaryWeblinkName);
     }
     if (!weblink) {
       weblink = this._firstCodeBrowserWeblink(weblinks);
     }
-    if (!weblink) { return null; }
+    if (!weblink) {
+      return null;
+    }
     return weblink;
   }
 
-  _getChangeWeblinks({repo, commit, options: {weblinks, config}}) {
+  _getChangeWeblinks(
+    params: GenerateWebLinksChangeParameters
+  ): GeneratedWebLink[] {
+    const weblinks = params.options?.weblinks;
+    const config = params.options?.config;
     if (!weblinks || !weblinks.length) return [];
     const commitWeblink = this._getBrowseCommitWeblink(weblinks, config);
-    return weblinks.filter(weblink =>
-      !commitWeblink ||
-      !commitWeblink.name ||
-      weblink.name !== commitWeblink.name);
+    return weblinks.filter(
+      weblink =>
+        !commitWeblink ||
+        !commitWeblink.name ||
+        weblink.name !== commitWeblink.name
+    );
   }
 
-  _getFileWebLinks({repo, commit, file, options: {weblinks}}) {
-    return weblinks;
+  _getFileWebLinks(params: GenerateWebLinksFileParameters): GeneratedWebLink[] {
+    return params.options?.weblinks || [];
   }
 
-  /**
-   * @param {!Object} params
-   * @return {string}
-   */
-  _generateSearchUrl(params) {
+  _generateSearchUrl(params: GenerateUrlSearchViewParameters) {
     let offsetExpr = '';
     if (params.offset && params.offset > 0) {
-      offsetExpr = ',' + params.offset;
+      offsetExpr = `,${params.offset}`;
     }
 
     if (params.query) {
       return '/q/' + encodeURL(params.query, true) + offsetExpr;
     }
 
-    const operators = [];
+    const operators: string[] = [];
     if (params.owner) {
       operators.push('owner:' + encodeURL(params.owner, false));
     }
@@ -400,32 +483,32 @@
       operators.push('topic:"' + encodeURL(params.topic, false) + '"');
     }
     if (params.hashtag) {
-      operators.push('hashtag:"' +
-          encodeURL(params.hashtag.toLowerCase(), false) + '"');
+      operators.push(
+        'hashtag:"' + encodeURL(params.hashtag.toLowerCase(), false) + '"'
+      );
     }
     if (params.statuses) {
       if (params.statuses.length === 1) {
-        operators.push(
-            'status:' + encodeURL(params.statuses[0], false));
+        operators.push('status:' + encodeURL(params.statuses[0], false));
       } else if (params.statuses.length > 1) {
         operators.push(
-            '(' +
-            params.statuses.map(s => `status:${encodeURL(s, false)}`)
-                .join(' OR ') +
-            ')');
+          '(' +
+            params.statuses
+              .map(s => `status:${encodeURL(s, false)}`)
+              .join(' OR ') +
+            ')'
+        );
       }
     }
 
     return '/q/' + operators.join('+') + offsetExpr;
   }
 
-  /**
-   * @param {!Object} params
-   * @return {string}
-   */
-  _generateChangeUrl(params) {
+  _generateChangeUrl(params: GenerateUrlChangeViewParameters) {
     let range = this._getPatchRangeExpression(params);
-    if (range.length) { range = '/' + range; }
+    if (range.length) {
+      range = '/' + range;
+    }
     let suffix = `${range}`;
     if (params.querystring) {
       suffix += '?' + params.querystring;
@@ -443,16 +526,14 @@
     }
   }
 
-  /**
-   * @param {!Object} params
-   * @return {string}
-   */
-  _generateDashboardUrl(params) {
-    const repoName = params.repo || params.project || null;
+  _generateDashboardUrl(params: GenerateUrlDashboardViewParameters) {
+    const repoName = params.repo || params.project || undefined;
     if (params.sections) {
       // Custom dashboard.
-      const queryParams = this._sectionsToEncodedParams(params.sections,
-          repoName);
+      const queryParams = this._sectionsToEncodedParams(
+        params.sections,
+        repoName
+      );
       if (params.title) {
         queryParams.push('title=' + encodeURIComponent(params.title));
       }
@@ -468,42 +549,44 @@
     }
   }
 
-  /**
-   * @param {!Array<!{name: string, query: string}>} sections
-   * @param {string=} opt_repoName
-   * @return {!Array<string>}
-   */
-  _sectionsToEncodedParams(sections, opt_repoName) {
+  _sectionsToEncodedParams(sections: DashboardSection[], repoName?: RepoName) {
     return sections.map(section => {
       // If there is a repo name provided, make sure to substitute it into the
       // ${repo} (or legacy ${project}) query tokens.
-      const query = opt_repoName ?
-        section.query.replace(REPO_TOKEN_PATTERN, opt_repoName) :
-        section.query;
-      return encodeURIComponent(section.name) + '=' +
-          encodeURIComponent(query);
+      const query = repoName
+        ? section.query.replace(REPO_TOKEN_PATTERN, repoName)
+        : section.query;
+      return encodeURIComponent(section.name) + '=' + encodeURIComponent(query);
     });
   }
 
   /**
-   * @param {!Object} params
-   * @return {string}
+   * @param params
+   * @return
    */
-  _generateDiffOrEditUrl(params) {
+  _generateDiffOrEditUrl(
+    params: GenerateUrlDiffViewParameters | GenerateUrlEditViewParameters
+  ) {
     let range = this._getPatchRangeExpression(params);
-    if (range.length) { range = '/' + range; }
+    if (range.length) {
+      range = '/' + range;
+    }
 
     let suffix = `${range}/${encodeURL(params.path || '', true)}`;
 
-    if (params.view === GerritNav.View.EDIT) { suffix += ',edit'; }
+    if (params.view === GerritView.EDIT) {
+      suffix += ',edit';
+    }
 
     if (params.lineNum) {
       suffix += '#';
-      if (params.leftSide) { suffix += 'b'; }
+      if (isGenerateUrlDiffViewParameters(params) && params.leftSide) {
+        suffix += 'b';
+      }
       suffix += params.lineNum;
     }
 
-    if (params.commentId) {
+    if (isGenerateUrlDiffViewParameters(params) && params.commentId) {
       suffix = `/comment/${params.commentId}` + suffix;
     }
 
@@ -515,45 +598,33 @@
     }
   }
 
-  /**
-   * @param {!Object} params
-   * @return {string}
-   */
-  _generateGroupUrl(params) {
-    let url = `/admin/groups/${encodeURL(params.groupId + '', true)}`;
-    if (params.detail === GerritNav.GroupDetailView.MEMBERS) {
+  _generateGroupUrl(params: GenerateUrlGroupViewParameters) {
+    let url = `/admin/groups/${encodeURL(`${params.groupId}`, true)}`;
+    if (params.detail === GroupDetailView.MEMBERS) {
       url += ',members';
-    } else if (params.detail === GerritNav.GroupDetailView.LOG) {
+    } else if (params.detail === GroupDetailView.LOG) {
       url += ',audit-log';
     }
     return url;
   }
 
-  /**
-   * @param {!Object} params
-   * @return {string}
-   */
-  _generateRepoUrl(params) {
-    let url = `/admin/repos/${encodeURL(params.repoName + '', true)}`;
-    if (params.detail === GerritNav.RepoDetailView.ACCESS) {
+  _generateRepoUrl(params: GenerateUrlRepoViewParameters) {
+    let url = `/admin/repos/${encodeURL(`${params.repoName}`, true)}`;
+    if (params.detail === RepoDetailView.ACCESS) {
       url += ',access';
-    } else if (params.detail === GerritNav.RepoDetailView.BRANCHES) {
+    } else if (params.detail === RepoDetailView.BRANCHES) {
       url += ',branches';
-    } else if (params.detail === GerritNav.RepoDetailView.TAGS) {
+    } else if (params.detail === RepoDetailView.TAGS) {
       url += ',tags';
-    } else if (params.detail === GerritNav.RepoDetailView.COMMANDS) {
+    } else if (params.detail === RepoDetailView.COMMANDS) {
       url += ',commands';
-    } else if (params.detail === GerritNav.RepoDetailView.DASHBOARDS) {
+    } else if (params.detail === RepoDetailView.DASHBOARDS) {
       url += ',dashboards';
     }
     return url;
   }
 
-  /**
-   * @param {!Object} params
-   * @return {string}
-   */
-  _generateSettingsUrl(params) {
+  _generateSettingsUrl() {
     return '/settings';
   }
 
@@ -561,62 +632,73 @@
    * Given an object of parameters, potentially including a `patchNum` or a
    * `basePatchNum` or both, return a string representation of that range. If
    * no range is indicated in the params, the empty string is returned.
-   *
-   * @param {!Object} params
-   * @return {string}
    */
-  _getPatchRangeExpression(params) {
+  _getPatchRangeExpression(params: PatchRangeParams) {
     let range = '';
-    if (params.patchNum) { range = '' + params.patchNum; }
-    if (params.basePatchNum) { range = params.basePatchNum + '..' + range; }
+    if (params.patchNum) {
+      range = `${params.patchNum}`;
+    }
+    if (params.basePatchNum) {
+      range = `${params.basePatchNum}..${range}`;
+    }
     return range;
   }
 
   /**
    * Given a set of params without a project, gets the project from the rest
    * API project lookup and then sets the app params.
-   *
-   * @param {?Object} params
    */
-  _normalizeLegacyRouteParams(params) {
-    if (!params.changeNum) { return Promise.resolve(); }
+  _normalizeLegacyRouteParams(
+    params: Readonly<
+      | GenerateUrlLegacyChangeViewParameters
+      | GenerateUrlLegacyDiffViewParameters
+    >
+  ) {
+    if (!params.changeNum) {
+      return Promise.resolve();
+    }
 
-    return this.$.restAPI.getFromProjectLookup(params.changeNum)
-        .then(project => {
-          // Show a 404 and terminate if the lookup request failed. Attempting
-          // to redirect after failing to get the project loops infinitely.
-          if (!project) {
-            this._show404();
-            return;
-          }
-
-          params.project = project;
-          this._normalizePatchRangeParams(params);
-          this._redirect(this._generateUrl(params));
-        });
+    return this.$.restAPI
+      .getFromProjectLookup(params.changeNum)
+      .then(project => {
+        // Show a 404 and terminate if the lookup request failed. Attempting
+        // to redirect after failing to get the project loops infinitely.
+        if (!project) {
+          this._show404();
+          return;
+        }
+        const updatedParams:
+          | GenerateUrlChangeViewParameters
+          | GenerateUrlDiffViewParameters = {...params, project};
+        this._normalizePatchRangeParams(updatedParams);
+        this._redirect(this._generateUrl(updatedParams));
+      });
   }
 
   /**
    * Normalizes the params object, and determines if the URL needs to be
    * modified to fit the proper schema.
    *
-   * @param {*} params
-   * @return {boolean} whether or not the URL needs to be upgraded.
+   * @param params
+   * @return whether or not the URL needs to be upgraded.
    */
-  _normalizePatchRangeParams(params) {
-    const hasBasePatchNum = params.basePatchNum !== null &&
-        params.basePatchNum !== undefined;
-    const hasPatchNum = params.patchNum !== null &&
-        params.patchNum !== undefined;
+  _normalizePatchRangeParams(params: PatchRangeParams) {
+    if (params.basePatchNum === null || params.basePatchNum === undefined) {
+      return false;
+    }
+    const hasPatchNum =
+      params.patchNum !== null && params.patchNum !== undefined;
     let needsRedirect = false;
 
     // Diffing a patch against itself is invalid, so if the base and revision
     // patches are equal clear the base.
-    if (hasBasePatchNum &&
-        patchNumEquals(params.basePatchNum, params.patchNum)) {
+    if (
+      params.patchNum &&
+      patchNumEquals(params.basePatchNum, params.patchNum)
+    ) {
       needsRedirect = true;
       params.basePatchNum = null;
-    } else if (hasBasePatchNum && !hasPatchNum) {
+    } else if (!hasPatchNum) {
       // Regexes set basePatchNum instead of patchNum when only one is
       // specified. Redirect is not needed in this case.
       params.patchNum = params.basePatchNum;
@@ -629,12 +711,11 @@
    * Redirect the user to login using the given return-URL for redirection
    * after authentication success.
    *
-   * @param {string} returnUrl
+   * @param returnUrl
    */
-  _redirectToLogin(returnUrl) {
+  _redirectToLogin(returnUrl: string) {
     const basePath = getBaseUrl() || '';
-    page(
-        '/login/' + encodeURIComponent(returnUrl.substring(basePath.length)));
+    page('/login/' + encodeURIComponent(returnUrl.substring(basePath.length)));
   }
 
   /**
@@ -642,17 +723,17 @@
    * is parsed to have a hash of "b" rather than "b#c". Instead, this method
    * parses hashes correctly. Will return an empty string if there is no hash.
    *
-   * @param {!string} canonicalPath
-   * @return {!string} Everything after the first '#' ("a#b#c" -> "b#c").
+   * @return Everything after the first '#' ("a#b#c" -> "b#c").
    */
-  _getHashFromCanonicalPath(canonicalPath) {
-    return canonicalPath.split('#').slice(1)
-        .join('#');
+  _getHashFromCanonicalPath(canonicalPath: string) {
+    return canonicalPath.split('#').slice(1).join('#');
   }
 
-  _parseLineAddress(hash) {
+  _parseLineAddress(hash: string) {
     const match = hash.match(LINE_ADDRESS_PATTERN);
-    if (!match) { return null; }
+    if (!match) {
+      return null;
+    }
     return {
       leftSide: !!match[1],
       lineNum: parseInt(match[2], 10),
@@ -664,11 +745,10 @@
    * resolves if the user is logged in. If the user us not logged in, the
    * promise is rejected and the page is redirected to the login flow.
    *
-   * @param {!Object} data The parsed route data.
-   * @return {!Promise<!Object>} A promise yielding the original route data
-   *     (if it resolves).
+   * @return A promise yielding the original route data
+   * (if it resolves).
    */
-  _redirectIfNotLoggedIn(data) {
+  _redirectIfNotLoggedIn(data: PageContext) {
     return this.$.restAPI.getLoggedIn().then(loggedIn => {
       if (loggedIn) {
         return Promise.resolve();
@@ -680,13 +760,15 @@
   }
 
   /**  Page.js middleware that warms the REST API's logged-in cache line. */
-  _loadUserMiddleware(ctx, next) {
-    this.$.restAPI.getLoggedIn().then(() => { next(); });
+  _loadUserMiddleware(_: PageContext, next: PageNextCallback) {
+    this.$.restAPI.getLoggedIn().then(() => {
+      next();
+    });
   }
 
   /**  Page.js middleware that try parse the querystring into queryMap. */
-  _queryStringMiddleware(ctx, next) {
-    let queryMap = new Map();
+  _queryStringMiddleware(ctx: PageContext, next: PageNextCallback) {
+    let queryMap: Map<string, string> | URLSearchParams = new Map();
     if (ctx.querystring) {
       // https://caniuse.com/#search=URLSearchParams
       if (window.URLSearchParams) {
@@ -695,38 +777,46 @@
         queryMap = new Map(this._parseQueryString(ctx.querystring));
       }
     }
-    ctx.queryMap = queryMap;
+    (ctx as PageContextWithQueryMap).queryMap = queryMap;
     next();
   }
 
   /**
    * Map a route to a method on the router.
    *
-   * @param {!string|!RegExp} pattern The page.js pattern for the route.
-   * @param {!string} handlerName The method name for the handler. If the
-   *     route is matched, the handler will be executed with `this` referring
-   *     to the component. Its return value will be discarded so that it does
-   *     not interfere with page.js.
-   * @param  {?boolean=} opt_authRedirect If true, then auth is checked before
-   *     executing the handler. If the user is not logged in, it will redirect
-   *     to the login flow and the handler will not be executed. The login
-   *     redirect specifies the matched URL to be used after successfull auth.
+   * @param pattern The page.js pattern for the route.
+   * @param handlerName The method name for the handler. If the
+   * route is matched, the handler will be executed with `this` referring
+   * to the component. Its return value will be discarded so that it does
+   * not interfere with page.js.
+   * @param authRedirect If true, then auth is checked before
+   * executing the handler. If the user is not logged in, it will redirect
+   * to the login flow and the handler will not be executed. The login
+   * redirect specifies the matched URL to be used after successfull auth.
    */
-  _mapRoute(pattern, handlerName, opt_authRedirect) {
+  _mapRoute(
+    pattern: string | RegExp,
+    handlerName: keyof GrRouter,
+    authRedirect?: boolean
+  ) {
     if (!this[handlerName]) {
-      console.error('Attempted to map route to unknown method: ',
-          handlerName);
+      console.error('Attempted to map route to unknown method: ', handlerName);
       return;
     }
-    page(pattern,
-        (ctx, next) => this._loadUserMiddleware(ctx, next),
-        (ctx, next) => this._queryStringMiddleware(ctx, next),
-        data => {
-          this.reporting.locationChanged(handlerName);
-          const promise = opt_authRedirect ?
-            this._redirectIfNotLoggedIn(data) : Promise.resolve();
-          promise.then(() => { this[handlerName](data); });
+    page(
+      pattern,
+      (ctx, next) => this._loadUserMiddleware(ctx, next),
+      (ctx, next) => this._queryStringMiddleware(ctx, next),
+      data => {
+        this.reporting.locationChanged(handlerName);
+        const promise = authRedirect
+          ? this._redirectIfNotLoggedIn(data)
+          : Promise.resolve();
+        promise.then(() => {
+          this[handlerName](data as PageContextWithQueryMap);
         });
+      }
+    );
   }
 
   _startRouter() {
@@ -736,19 +826,19 @@
     }
 
     GerritNav.setup(
-        (url, opt_redirect) => {
-          if (opt_redirect) {
-            page.redirect(url);
-          } else {
-            page.show(url);
-          }
-        },
-        this._generateUrl.bind(this),
-        params => this._generateWeblinks(params),
-        x => x
+      (url, redirect?) => {
+        if (redirect) {
+          page.redirect(url);
+        } else {
+          page.show(url);
+        }
+      },
+      params => this._generateUrl(params),
+      params => this._generateWeblinks(params),
+      x => x
     );
 
-    page.exit('*', (ctx, next) => {
+    page.exit('*', (_, next) => {
       if (!this._isRedirecting) {
         this.reporting.beforeLocationChanged();
       }
@@ -772,13 +862,16 @@
       // Fire asynchronously so that the URL is changed by the time the event
       // is processed.
       this.async(() => {
-        this.dispatchEvent(new CustomEvent('location-change', {
-          detail: {
-            hash: window.location.hash,
-            pathname: window.location.pathname,
-          },
-          composed: true, bubbles: true,
-        }));
+        this.dispatchEvent(
+          new CustomEvent('location-change', {
+            detail: {
+              hash: window.location.hash,
+              pathname: window.location.pathname,
+            },
+            composed: true,
+            bubbles: true,
+          })
+        );
       }, 1);
       next();
     });
@@ -787,103 +880,150 @@
 
     this._mapRoute(RoutePattern.DASHBOARD, '_handleDashboardRoute');
 
-    this._mapRoute(RoutePattern.CUSTOM_DASHBOARD,
-        '_handleCustomDashboardRoute');
+    this._mapRoute(
+      RoutePattern.CUSTOM_DASHBOARD,
+      '_handleCustomDashboardRoute'
+    );
 
-    this._mapRoute(RoutePattern.PROJECT_DASHBOARD,
-        '_handleProjectDashboardRoute');
+    this._mapRoute(
+      RoutePattern.PROJECT_DASHBOARD,
+      '_handleProjectDashboardRoute'
+    );
 
     this._mapRoute(RoutePattern.GROUP_INFO, '_handleGroupInfoRoute', true);
 
-    this._mapRoute(RoutePattern.GROUP_AUDIT_LOG, '_handleGroupAuditLogRoute',
-        true);
+    this._mapRoute(
+      RoutePattern.GROUP_AUDIT_LOG,
+      '_handleGroupAuditLogRoute',
+      true
+    );
 
-    this._mapRoute(RoutePattern.GROUP_MEMBERS, '_handleGroupMembersRoute',
-        true);
+    this._mapRoute(
+      RoutePattern.GROUP_MEMBERS,
+      '_handleGroupMembersRoute',
+      true
+    );
 
-    this._mapRoute(RoutePattern.GROUP_LIST_OFFSET,
-        '_handleGroupListOffsetRoute', true);
+    this._mapRoute(
+      RoutePattern.GROUP_LIST_OFFSET,
+      '_handleGroupListOffsetRoute',
+      true
+    );
 
-    this._mapRoute(RoutePattern.GROUP_LIST_FILTER_OFFSET,
-        '_handleGroupListFilterOffsetRoute', true);
+    this._mapRoute(
+      RoutePattern.GROUP_LIST_FILTER_OFFSET,
+      '_handleGroupListFilterOffsetRoute',
+      true
+    );
 
-    this._mapRoute(RoutePattern.GROUP_LIST_FILTER,
-        '_handleGroupListFilterRoute', true);
+    this._mapRoute(
+      RoutePattern.GROUP_LIST_FILTER,
+      '_handleGroupListFilterRoute',
+      true
+    );
 
-    this._mapRoute(RoutePattern.GROUP_SELF, '_handleGroupSelfRedirectRoute',
-        true);
+    this._mapRoute(
+      RoutePattern.GROUP_SELF,
+      '_handleGroupSelfRedirectRoute',
+      true
+    );
 
     this._mapRoute(RoutePattern.GROUP, '_handleGroupRoute', true);
 
-    this._mapRoute(RoutePattern.PROJECT_OLD,
-        '_handleProjectsOldRoute');
+    this._mapRoute(RoutePattern.PROJECT_OLD, '_handleProjectsOldRoute');
 
-    this._mapRoute(RoutePattern.REPO_COMMANDS,
-        '_handleRepoCommandsRoute', true);
+    this._mapRoute(
+      RoutePattern.REPO_COMMANDS,
+      '_handleRepoCommandsRoute',
+      true
+    );
 
-    this._mapRoute(RoutePattern.REPO_ACCESS,
-        '_handleRepoAccessRoute');
+    this._mapRoute(RoutePattern.REPO_ACCESS, '_handleRepoAccessRoute');
 
-    this._mapRoute(RoutePattern.REPO_DASHBOARDS,
-        '_handleRepoDashboardsRoute');
+    this._mapRoute(RoutePattern.REPO_DASHBOARDS, '_handleRepoDashboardsRoute');
 
-    this._mapRoute(RoutePattern.BRANCH_LIST_OFFSET,
-        '_handleBranchListOffsetRoute');
+    this._mapRoute(
+      RoutePattern.BRANCH_LIST_OFFSET,
+      '_handleBranchListOffsetRoute'
+    );
 
-    this._mapRoute(RoutePattern.BRANCH_LIST_FILTER_OFFSET,
-        '_handleBranchListFilterOffsetRoute');
+    this._mapRoute(
+      RoutePattern.BRANCH_LIST_FILTER_OFFSET,
+      '_handleBranchListFilterOffsetRoute'
+    );
 
-    this._mapRoute(RoutePattern.BRANCH_LIST_FILTER,
-        '_handleBranchListFilterRoute');
+    this._mapRoute(
+      RoutePattern.BRANCH_LIST_FILTER,
+      '_handleBranchListFilterRoute'
+    );
 
-    this._mapRoute(RoutePattern.TAG_LIST_OFFSET,
-        '_handleTagListOffsetRoute');
+    this._mapRoute(RoutePattern.TAG_LIST_OFFSET, '_handleTagListOffsetRoute');
 
-    this._mapRoute(RoutePattern.TAG_LIST_FILTER_OFFSET,
-        '_handleTagListFilterOffsetRoute');
+    this._mapRoute(
+      RoutePattern.TAG_LIST_FILTER_OFFSET,
+      '_handleTagListFilterOffsetRoute'
+    );
 
-    this._mapRoute(RoutePattern.TAG_LIST_FILTER,
-        '_handleTagListFilterRoute');
+    this._mapRoute(RoutePattern.TAG_LIST_FILTER, '_handleTagListFilterRoute');
 
-    this._mapRoute(RoutePattern.LEGACY_CREATE_GROUP,
-        '_handleCreateGroupRoute', true);
+    this._mapRoute(
+      RoutePattern.LEGACY_CREATE_GROUP,
+      '_handleCreateGroupRoute',
+      true
+    );
 
-    this._mapRoute(RoutePattern.LEGACY_CREATE_PROJECT,
-        '_handleCreateProjectRoute', true);
+    this._mapRoute(
+      RoutePattern.LEGACY_CREATE_PROJECT,
+      '_handleCreateProjectRoute',
+      true
+    );
 
-    this._mapRoute(RoutePattern.REPO_LIST_OFFSET,
-        '_handleRepoListOffsetRoute');
+    this._mapRoute(RoutePattern.REPO_LIST_OFFSET, '_handleRepoListOffsetRoute');
 
-    this._mapRoute(RoutePattern.REPO_LIST_FILTER_OFFSET,
-        '_handleRepoListFilterOffsetRoute');
+    this._mapRoute(
+      RoutePattern.REPO_LIST_FILTER_OFFSET,
+      '_handleRepoListFilterOffsetRoute'
+    );
 
-    this._mapRoute(RoutePattern.REPO_LIST_FILTER,
-        '_handleRepoListFilterRoute');
+    this._mapRoute(RoutePattern.REPO_LIST_FILTER, '_handleRepoListFilterRoute');
 
     this._mapRoute(RoutePattern.REPO, '_handleRepoRoute');
 
     this._mapRoute(RoutePattern.PLUGINS, '_handlePassThroughRoute');
 
-    this._mapRoute(RoutePattern.PLUGIN_LIST_OFFSET,
-        '_handlePluginListOffsetRoute', true);
+    this._mapRoute(
+      RoutePattern.PLUGIN_LIST_OFFSET,
+      '_handlePluginListOffsetRoute',
+      true
+    );
 
-    this._mapRoute(RoutePattern.PLUGIN_LIST_FILTER_OFFSET,
-        '_handlePluginListFilterOffsetRoute', true);
+    this._mapRoute(
+      RoutePattern.PLUGIN_LIST_FILTER_OFFSET,
+      '_handlePluginListFilterOffsetRoute',
+      true
+    );
 
-    this._mapRoute(RoutePattern.PLUGIN_LIST_FILTER,
-        '_handlePluginListFilterRoute', true);
+    this._mapRoute(
+      RoutePattern.PLUGIN_LIST_FILTER,
+      '_handlePluginListFilterRoute',
+      true
+    );
 
     this._mapRoute(RoutePattern.PLUGIN_LIST, '_handlePluginListRoute', true);
 
-    this._mapRoute(RoutePattern.QUERY_LEGACY_SUFFIX,
-        '_handleQueryLegacySuffixRoute');
+    this._mapRoute(
+      RoutePattern.QUERY_LEGACY_SUFFIX,
+      '_handleQueryLegacySuffixRoute'
+    );
 
     this._mapRoute(RoutePattern.QUERY, '_handleQueryRoute');
 
     this._mapRoute(RoutePattern.DIFF_LEGACY_LINENUM, '_handleLegacyLinenum');
 
-    this._mapRoute(RoutePattern.CHANGE_NUMBER_LEGACY,
-        '_handleChangeNumberLegacyRoute');
+    this._mapRoute(
+      RoutePattern.CHANGE_NUMBER_LEGACY,
+      '_handleChangeNumberLegacyRoute'
+    );
 
     this._mapRoute(RoutePattern.DIFF_EDIT, '_handleDiffEditRoute', true);
 
@@ -901,11 +1041,17 @@
 
     this._mapRoute(RoutePattern.AGREEMENTS, '_handleAgreementsRoute', true);
 
-    this._mapRoute(RoutePattern.NEW_AGREEMENTS, '_handleNewAgreementsRoute',
-        true);
+    this._mapRoute(
+      RoutePattern.NEW_AGREEMENTS,
+      '_handleNewAgreementsRoute',
+      true
+    );
 
-    this._mapRoute(RoutePattern.SETTINGS_LEGACY,
-        '_handleSettingsLegacyRoute', true);
+    this._mapRoute(
+      RoutePattern.SETTINGS_LEGACY,
+      '_handleSettingsLegacyRoute',
+      true
+    );
 
     this._mapRoute(RoutePattern.SETTINGS, '_handleSettingsRoute', true);
 
@@ -913,21 +1059,29 @@
 
     this._mapRoute(RoutePattern.LOG_IN_OR_OUT, '_handlePassThroughRoute');
 
-    this._mapRoute(RoutePattern.IMPROPERLY_ENCODED_PLUS,
-        '_handleImproperlyEncodedPlusRoute');
+    this._mapRoute(
+      RoutePattern.IMPROPERLY_ENCODED_PLUS,
+      '_handleImproperlyEncodedPlusRoute'
+    );
 
     this._mapRoute(RoutePattern.PLUGIN_SCREEN, '_handlePluginScreen');
 
-    this._mapRoute(RoutePattern.DOCUMENTATION_SEARCH_FILTER,
-        '_handleDocumentationSearchRoute');
+    this._mapRoute(
+      RoutePattern.DOCUMENTATION_SEARCH_FILTER,
+      '_handleDocumentationSearchRoute'
+    );
 
     // redirects /Documentation/q/* to /Documentation/q/filter:*
-    this._mapRoute(RoutePattern.DOCUMENTATION_SEARCH,
-        '_handleDocumentationSearchRedirectRoute');
+    this._mapRoute(
+      RoutePattern.DOCUMENTATION_SEARCH,
+      '_handleDocumentationSearchRedirectRoute'
+    );
 
     // Makes sure /Documentation/* links work (doin't return 404)
-    this._mapRoute(RoutePattern.DOCUMENTATION,
-        '_handleDocumentationRedirectRoute');
+    this._mapRoute(
+      RoutePattern.DOCUMENTATION,
+      '_handleDocumentationRedirectRoute'
+    );
 
     // Note: this route should appear last so it only catches URLs unmatched
     // by other patterns.
@@ -937,11 +1091,10 @@
   }
 
   /**
-   * @param {!Object} data
-   * @return {Promise|null} if handling the route involves asynchrony, then a
-   *     promise is returned. Otherwise, synchronous handling returns null.
+   * @return if handling the route involves asynchrony, then a
+   * promise is returned. Otherwise, synchronous handling returns null.
    */
-  _handleRootRoute(data) {
+  _handleRootRoute(data: PageContextWithQueryMap) {
     if (data.querystring.match(/^closeAfterLogin/)) {
       // Close child window on redirect after login.
       window.close();
@@ -980,10 +1133,10 @@
   /**
    * Decode an application/x-www-form-urlencoded string.
    *
-   * @param {string} qs The application/x-www-form-urlencoded string.
-   * @return {string} The decoded string.
+   * @param qs The application/x-www-form-urlencoded string.
+   * @return The decoded string.
    */
-  _decodeQueryString(qs) {
+  _decodeQueryString(qs: string) {
     return decodeURIComponent(qs.replace(PLUS_PATTERN, ' '));
   }
 
@@ -991,16 +1144,16 @@
    * Parse a query string (e.g. window.location.search) into an array of
    * name/value pairs.
    *
-   * @param {string} qs The application/x-www-form-urlencoded query string.
-   * @return {!Array<!Array<string>>} An array of name/value pairs, where each
-   *     element is a 2-element array.
+   * @param qs The application/x-www-form-urlencoded query string.
+   * @return An array of name/value pairs, where each
+   * element is a 2-element array.
    */
-  _parseQueryString(qs) {
+  _parseQueryString(qs: string): Array<QueryStringItem> {
     qs = qs.replace(QUESTION_PATTERN, '');
     if (!qs) {
       return [];
     }
-    const params = [];
+    const params: Array<[string, string]> = [];
     qs.split('&').forEach(param => {
       const idx = param.indexOf('=');
       let name;
@@ -1021,10 +1174,8 @@
 
   /**
    * Handle dashboard routes. These may be user, or project dashboards.
-   *
-   * @param {!Object} data The parsed route data.
    */
-  _handleDashboardRoute(data) {
+  _handleDashboardRoute(data: PageContextWithQueryMap) {
     // User dashboard. We require viewing user to be logged in, else we
     // redirect to login for self dashboard or simple owner search for
     // other user dashboard.
@@ -1037,7 +1188,7 @@
         }
       } else {
         this._setParams({
-          view: GerritNav.View.DASHBOARD,
+          view: GerritView.DASHBOARD,
           user: data.params[0],
         });
       }
@@ -1047,31 +1198,37 @@
   /**
    * Handle custom dashboard routes.
    *
-   * @param {!Object} data The parsed route data.
-   * @param {string=} opt_qs Optional query string associated with the route.
-   *     If not given, window.location.search is used. (Used by tests).
+   * @param qs Optional query string associated with the route.
+   * If not given, window.location.search is used. (Used by tests).
    */
-  _handleCustomDashboardRoute(data, opt_qs) {
-    // opt_qs may be provided by a test, and it may have a falsy value
-    const qs = opt_qs !== undefined ? opt_qs : window.location.search;
+  _handleCustomDashboardRoute(
+    _: PageContextWithQueryMap,
+    qs: string = window.location.search
+  ) {
     const queryParams = this._parseQueryString(qs);
     let title = 'Custom Dashboard';
     const titleParam = queryParams.find(
-        elem => elem[0].toLowerCase() === 'title');
+      elem => elem[0].toLowerCase() === 'title'
+    );
     if (titleParam) {
       title = titleParam[1];
     }
     // Dashboards support a foreach param which adds a base query to any
     // additional query.
     const forEachParam = queryParams.find(
-        elem => elem[0].toLowerCase() === 'foreach');
-    let forEachQuery = null;
+      elem => elem[0].toLowerCase() === 'foreach'
+    );
+    let forEachQuery: string | null = null;
     if (forEachParam) {
       forEachQuery = forEachParam[1];
     }
     const sectionParams = queryParams.filter(
-        elem => elem[0] && elem[1] && elem[0].toLowerCase() !== 'title' &&
-        elem[0].toLowerCase() !== 'foreach');
+      elem =>
+        elem[0] &&
+        elem[1] &&
+        elem[0].toLowerCase() !== 'title' &&
+        elem[0].toLowerCase() !== 'foreach'
+    );
     const sections = sectionParams.map(elem => {
       const query = forEachQuery ? `${forEachQuery} ${elem[1]}` : elem[1];
       return {
@@ -1083,7 +1240,7 @@
     if (sections.length > 0) {
       // Custom dashboard view.
       this._setParams({
-        view: GerritNav.View.DASHBOARD,
+        view: GerritView.DASHBOARD,
         user: 'self',
         sections,
         title,
@@ -1096,50 +1253,50 @@
     return Promise.resolve();
   }
 
-  _handleProjectDashboardRoute(data) {
-    const project = data.params[0];
+  _handleProjectDashboardRoute(data: PageContextWithQueryMap) {
+    const project = data.params[0] as RepoName;
     this._setParams({
-      view: GerritNav.View.DASHBOARD,
+      view: GerritView.DASHBOARD,
       project,
       dashboard: decodeURIComponent(data.params[1]),
     });
     this.reporting.setRepoName(project);
   }
 
-  _handleGroupInfoRoute(data) {
+  _handleGroupInfoRoute(data: PageContextWithQueryMap) {
     this._redirect('/admin/groups/' + encodeURIComponent(data.params[0]));
   }
 
-  _handleGroupSelfRedirectRoute(data) {
+  _handleGroupSelfRedirectRoute(_: PageContextWithQueryMap) {
     this._redirect('/settings/#Groups');
   }
 
-  _handleGroupRoute(data) {
+  _handleGroupRoute(data: PageContextWithQueryMap) {
     this._setParams({
-      view: GerritNav.View.GROUP,
-      groupId: data.params[0],
+      view: GerritView.GROUP,
+      groupId: data.params[0] as GroupId,
     });
   }
 
-  _handleGroupAuditLogRoute(data) {
+  _handleGroupAuditLogRoute(data: PageContextWithQueryMap) {
     this._setParams({
-      view: GerritNav.View.GROUP,
-      detail: GerritNav.GroupDetailView.LOG,
-      groupId: data.params[0],
+      view: GerritView.GROUP,
+      detail: GroupDetailView.LOG,
+      groupId: data.params[0] as GroupId,
     });
   }
 
-  _handleGroupMembersRoute(data) {
+  _handleGroupMembersRoute(data: PageContextWithQueryMap) {
     this._setParams({
-      view: GerritNav.View.GROUP,
-      detail: GerritNav.GroupDetailView.MEMBERS,
-      groupId: data.params[0],
+      view: GerritView.GROUP,
+      detail: GroupDetailView.MEMBERS,
+      groupId: data.params[0] as GroupId,
     });
   }
 
-  _handleGroupListOffsetRoute(data) {
+  _handleGroupListOffsetRoute(data: PageContextWithQueryMap) {
     this._setParams({
-      view: GerritNav.View.ADMIN,
+      view: GerritView.ADMIN,
       adminView: 'gr-admin-group-list',
       offset: data.params[1] || 0,
       filter: null,
@@ -1147,127 +1304,126 @@
     });
   }
 
-  _handleGroupListFilterOffsetRoute(data) {
+  _handleGroupListFilterOffsetRoute(data: PageContextWithQueryMap) {
     this._setParams({
-      view: GerritNav.View.ADMIN,
+      view: GerritView.ADMIN,
       adminView: 'gr-admin-group-list',
-      offset: data.params.offset,
-      filter: data.params.filter,
+      offset: data.params['offset'],
+      filter: data.params['filter'],
     });
   }
 
-  _handleGroupListFilterRoute(data) {
+  _handleGroupListFilterRoute(data: PageContextWithQueryMap) {
     this._setParams({
-      view: GerritNav.View.ADMIN,
+      view: GerritView.ADMIN,
       adminView: 'gr-admin-group-list',
-      filter: data.params.filter || null,
+      filter: data.params['filter'] || null,
     });
   }
 
-  _handleProjectsOldRoute(data) {
+  _handleProjectsOldRoute(data: PageContextWithQueryMap) {
     let params = '';
     if (data.params[1]) {
       params = encodeURIComponent(data.params[1]);
       if (data.params[1].includes(',')) {
-        params =
-            encodeURIComponent(data.params[1]).replace('%2C', ',');
+        params = encodeURIComponent(data.params[1]).replace('%2C', ',');
       }
     }
 
     this._redirect(`/admin/repos/${params}`);
   }
 
-  _handleRepoCommandsRoute(data) {
-    const repo = data.params[0];
+  _handleRepoCommandsRoute(data: PageContextWithQueryMap) {
+    const repo = data.params[0] as RepoName;
     this._setParams({
-      view: GerritNav.View.REPO,
-      detail: GerritNav.RepoDetailView.COMMANDS,
+      view: GerritView.REPO,
+      detail: RepoDetailView.COMMANDS,
       repo,
     });
     this.reporting.setRepoName(repo);
   }
 
-  _handleRepoAccessRoute(data) {
-    const repo = data.params[0];
+  _handleRepoAccessRoute(data: PageContextWithQueryMap) {
+    const repo = data.params[0] as RepoName;
     this._setParams({
-      view: GerritNav.View.REPO,
-      detail: GerritNav.RepoDetailView.ACCESS,
+      view: GerritView.REPO,
+      detail: RepoDetailView.ACCESS,
       repo,
     });
     this.reporting.setRepoName(repo);
   }
 
-  _handleRepoDashboardsRoute(data) {
-    const repo = data.params[0];
+  _handleRepoDashboardsRoute(data: PageContextWithQueryMap) {
+    const repo = data.params[0] as RepoName;
     this._setParams({
-      view: GerritNav.View.REPO,
-      detail: GerritNav.RepoDetailView.DASHBOARDS,
+      view: GerritView.REPO,
+      detail: RepoDetailView.DASHBOARDS,
       repo,
     });
     this.reporting.setRepoName(repo);
   }
 
-  _handleBranchListOffsetRoute(data) {
+  _handleBranchListOffsetRoute(data: PageContextWithQueryMap) {
     this._setParams({
-      view: GerritNav.View.REPO,
-      detail: GerritNav.RepoDetailView.BRANCHES,
-      repo: data.params[0],
+      view: GerritView.REPO,
+      detail: RepoDetailView.BRANCHES,
+      repo: data.params[0] as RepoName,
       offset: data.params[2] || 0,
       filter: null,
     });
   }
 
-  _handleBranchListFilterOffsetRoute(data) {
+  _handleBranchListFilterOffsetRoute(data: PageContextWithQueryMap) {
     this._setParams({
-      view: GerritNav.View.REPO,
-      detail: GerritNav.RepoDetailView.BRANCHES,
-      repo: data.params.repo,
-      offset: data.params.offset,
-      filter: data.params.filter,
+      view: GerritView.REPO,
+      detail: RepoDetailView.BRANCHES,
+      repo: data.params['repo'] as RepoName,
+      offset: data.params['offset'],
+      filter: data.params['filter'],
     });
   }
 
-  _handleBranchListFilterRoute(data) {
+  _handleBranchListFilterRoute(data: PageContextWithQueryMap) {
     this._setParams({
-      view: GerritNav.View.REPO,
-      detail: GerritNav.RepoDetailView.BRANCHES,
-      repo: data.params.repo,
-      filter: data.params.filter || null,
+      view: GerritView.REPO,
+      detail: RepoDetailView.BRANCHES,
+      repo: data.params['repo'] as RepoName,
+      filter: data.params['filter'] || null,
     });
   }
 
-  _handleTagListOffsetRoute(data) {
+  _handleTagListOffsetRoute(data: PageContextWithQueryMap) {
     this._setParams({
-      view: GerritNav.View.REPO,
-      detail: GerritNav.RepoDetailView.TAGS,
-      repo: data.params[0],
+      view: GerritView.REPO,
+      detail: RepoDetailView.TAGS,
+      repo: data.params[0] as RepoName,
       offset: data.params[2] || 0,
       filter: null,
     });
   }
 
-  _handleTagListFilterOffsetRoute(data) {
+  _handleTagListFilterOffsetRoute(data: PageContextWithQueryMap) {
     this._setParams({
-      view: GerritNav.View.REPO,
-      detail: GerritNav.RepoDetailView.TAGS,
-      repo: data.params.repo,
-      offset: data.params.offset,
-      filter: data.params.filter,
+      view: GerritView.REPO,
+      detail: RepoDetailView.TAGS,
+      repo: data.params['repo'] as RepoName,
+      offset: data.params['offset'],
+      filter: data.params['filter'],
     });
   }
 
-  _handleTagListFilterRoute(data) {
+  _handleTagListFilterRoute(data: PageContextWithQueryMap) {
     this._setParams({
-      view: GerritNav.View.REPO,
-      detail: GerritNav.RepoDetailView.TAGS,
-      repo: data.params.repo,
-      filter: data.params.filter || null,
+      view: GerritView.REPO,
+      detail: RepoDetailView.TAGS,
+      repo: data.params['repo'] as RepoName,
+      filter: data.params['filter'] || null,
     });
   }
 
-  _handleRepoListOffsetRoute(data) {
+  _handleRepoListOffsetRoute(data: PageContextWithQueryMap) {
     this._setParams({
-      view: GerritNav.View.ADMIN,
+      view: GerritView.ADMIN,
       adminView: 'gr-repo-list',
       offset: data.params[1] || 0,
       filter: null,
@@ -1275,101 +1431,102 @@
     });
   }
 
-  _handleRepoListFilterOffsetRoute(data) {
+  _handleRepoListFilterOffsetRoute(data: PageContextWithQueryMap) {
     this._setParams({
-      view: GerritNav.View.ADMIN,
+      view: GerritView.ADMIN,
       adminView: 'gr-repo-list',
-      offset: data.params.offset,
-      filter: data.params.filter,
+      offset: data.params['offset'],
+      filter: data.params['filter'],
     });
   }
 
-  _handleRepoListFilterRoute(data) {
+  _handleRepoListFilterRoute(data: PageContextWithQueryMap) {
     this._setParams({
-      view: GerritNav.View.ADMIN,
+      view: GerritView.ADMIN,
       adminView: 'gr-repo-list',
-      filter: data.params.filter || null,
+      filter: data.params['filter'] || null,
     });
   }
 
-  _handleCreateProjectRoute(data) {
+  _handleCreateProjectRoute(_: PageContextWithQueryMap) {
     // Redirects the legacy route to the new route, which displays the project
     // list with a hash 'create'.
     this._redirect('/admin/repos#create');
   }
 
-  _handleCreateGroupRoute(data) {
+  _handleCreateGroupRoute(_: PageContextWithQueryMap) {
     // Redirects the legacy route to the new route, which displays the group
     // list with a hash 'create'.
     this._redirect('/admin/groups#create');
   }
 
-  _handleRepoRoute(data) {
-    const repo = data.params[0];
+  _handleRepoRoute(data: PageContextWithQueryMap) {
+    const repo = data.params[0] as RepoName;
     this._setParams({
-      view: GerritNav.View.REPO,
+      view: GerritView.REPO,
       repo,
     });
     this.reporting.setRepoName(repo);
   }
 
-  _handlePluginListOffsetRoute(data) {
+  _handlePluginListOffsetRoute(data: PageContextWithQueryMap) {
     this._setParams({
-      view: GerritNav.View.ADMIN,
+      view: GerritView.ADMIN,
       adminView: 'gr-plugin-list',
       offset: data.params[1] || 0,
       filter: null,
     });
   }
 
-  _handlePluginListFilterOffsetRoute(data) {
+  _handlePluginListFilterOffsetRoute(data: PageContextWithQueryMap) {
     this._setParams({
-      view: GerritNav.View.ADMIN,
+      view: GerritView.ADMIN,
       adminView: 'gr-plugin-list',
-      offset: data.params.offset,
-      filter: data.params.filter,
+      offset: data.params['offset'],
+      filter: data.params['filter'],
     });
   }
 
-  _handlePluginListFilterRoute(data) {
+  _handlePluginListFilterRoute(data: PageContextWithQueryMap) {
     this._setParams({
-      view: GerritNav.View.ADMIN,
+      view: GerritView.ADMIN,
       adminView: 'gr-plugin-list',
-      filter: data.params.filter || null,
+      filter: data.params['filter'] || null,
     });
   }
 
-  _handlePluginListRoute(data) {
+  _handlePluginListRoute(_: PageContextWithQueryMap) {
     this._setParams({
-      view: GerritNav.View.ADMIN,
+      view: GerritView.ADMIN,
       adminView: 'gr-plugin-list',
     });
   }
 
-  _handleQueryRoute(data) {
+  _handleQueryRoute(data: PageContextWithQueryMap) {
     this._setParams({
-      view: GerritNav.View.SEARCH,
+      view: GerritView.SEARCH,
       query: data.params[0],
       offset: data.params[2],
     });
   }
 
-  _handleQueryLegacySuffixRoute(ctx) {
+  _handleQueryLegacySuffixRoute(ctx: PageContextWithQueryMap) {
     this._redirect(ctx.path.replace(LEGACY_QUERY_SUFFIX_PATTERN, ''));
   }
 
-  _handleChangeNumberLegacyRoute(ctx) {
+  _handleChangeNumberLegacyRoute(ctx: PageContextWithQueryMap) {
     this._redirect('/c/' + encodeURIComponent(ctx.params[0]));
   }
 
-  _handleChangeRoute(ctx) {
+  _handleChangeRoute(ctx: PageContextWithQueryMap) {
     // Parameter order is based on the regex group number matched.
-    const params = {
-      project: ctx.params[0],
-      changeNum: ctx.params[1],
-      basePatchNum: ctx.params[4],
-      patchNum: ctx.params[6],
-      view: GerritNav.View.CHANGE,
+    const params: GenerateUrlChangeViewParameters = {
+      project: ctx.params[0] as RepoName,
+      // TODO(TS): remove as unknown
+      changeNum: (ctx.params[1] as unknown) as NumericChangeId,
+      basePatchNum: ctx.params[4] as PatchSetNum,
+      patchNum: ctx.params[6] as PatchSetNum,
+      view: GerritView.CHANGE,
       queryMap: ctx.queryMap,
     };
 
@@ -1377,27 +1534,27 @@
     this._redirectOrNavigate(params);
   }
 
-  _handleCommentRoute(ctx) {
-    const params = {
-      project: ctx.params[0],
-      changeNum: ctx.params[1],
-      commentId: ctx.params[2],
-      view: GerritNav.View.DIFF,
+  _handleCommentRoute(ctx: PageContextWithQueryMap) {
+    const params: GenerateUrlDiffViewParameters = {
+      project: ctx.params[0] as RepoName,
+      changeNum: (ctx.params[1] as unknown) as NumericChangeId,
+      commentId: ctx.params[2] as UrlEncodedCommentId,
+      view: GerritView.DIFF,
       commentLink: true,
     };
     this.reporting.setRepoName(params.project);
     this._redirectOrNavigate(params);
   }
 
-  _handleDiffRoute(ctx) {
+  _handleDiffRoute(ctx: PageContextWithQueryMap) {
     // Parameter order is based on the regex group number matched.
-    const params = {
-      project: ctx.params[0],
-      changeNum: ctx.params[1],
-      basePatchNum: ctx.params[4],
-      patchNum: ctx.params[6],
+    const params: GenerateUrlDiffViewParameters = {
+      project: ctx.params[0] as RepoName,
+      changeNum: (ctx.params[1] as unknown) as NumericChangeId,
+      basePatchNum: ctx.params[4] as PatchSetNum,
+      patchNum: ctx.params[6] as PatchSetNum,
       path: ctx.params[8],
-      view: GerritNav.View.DIFF,
+      view: GerritView.DIFF,
     };
     const address = this._parseLineAddress(ctx.hash);
     if (address) {
@@ -1408,31 +1565,32 @@
     this._redirectOrNavigate(params);
   }
 
-  _handleChangeLegacyRoute(ctx) {
+  _handleChangeLegacyRoute(ctx: PageContextWithQueryMap) {
     // Parameter order is based on the regex group number matched.
-    const params = {
-      changeNum: ctx.params[0],
-      basePatchNum: ctx.params[3],
-      patchNum: ctx.params[5],
-      view: GerritNav.View.CHANGE,
+    const params: GenerateUrlLegacyChangeViewParameters = {
+      changeNum: (ctx.params[0] as unknown) as NumericChangeId,
+      basePatchNum: ctx.params[3] as PatchSetNum,
+      patchNum: ctx.params[5] as PatchSetNum,
+      view: GerritView.CHANGE,
       querystring: ctx.querystring,
     };
 
     this._normalizeLegacyRouteParams(params);
   }
 
-  _handleLegacyLinenum(ctx) {
+  _handleLegacyLinenum(ctx: PageContextWithQueryMap) {
     this._redirect(ctx.path.replace(LEGACY_LINENUM_PATTERN, '#$1'));
   }
 
-  _handleDiffLegacyRoute(ctx) {
+  _handleDiffLegacyRoute(ctx: PageContextWithQueryMap) {
     // Parameter order is based on the regex group number matched.
-    const params = {
-      changeNum: ctx.params[0],
-      basePatchNum: ctx.params[2],
-      patchNum: ctx.params[4],
+    const params: GenerateUrlLegacyDiffViewParameters = {
+      // TODO(TS): remove "as unknown"
+      changeNum: (ctx.params[0] as unknown) as NumericChangeId,
+      basePatchNum: ctx.params[2] as PatchSetNum,
+      patchNum: ctx.params[4] as PatchSetNum,
       path: ctx.params[5],
-      view: GerritNav.View.DIFF,
+      view: GerritView.DIFF,
     };
 
     const address = this._parseLineAddress(ctx.hash);
@@ -1444,28 +1602,29 @@
     this._normalizeLegacyRouteParams(params);
   }
 
-  _handleDiffEditRoute(ctx) {
+  _handleDiffEditRoute(ctx: PageContextWithQueryMap) {
     // Parameter order is based on the regex group number matched.
-    const project = ctx.params[0];
+    const project = ctx.params[0] as RepoName;
     this._redirectOrNavigate({
       project,
-      changeNum: ctx.params[1],
-      patchNum: ctx.params[2],
+      changeNum: (ctx.params[1] as unknown) as NumericChangeId,
+      patchNum: ctx.params[2] as PatchSetNum,
       path: ctx.params[3],
       lineNum: ctx.hash,
-      view: GerritNav.View.EDIT,
+      view: GerritView.EDIT,
     });
     this.reporting.setRepoName(project);
   }
 
-  _handleChangeEditRoute(ctx) {
+  _handleChangeEditRoute(ctx: PageContextWithQueryMap) {
     // Parameter order is based on the regex group number matched.
-    const project = ctx.params[0];
+    const project = ctx.params[0] as RepoName;
     this._redirectOrNavigate({
       project,
-      changeNum: ctx.params[1],
-      patchNum: ctx.params[3],
-      view: GerritNav.View.CHANGE,
+      // TODO(TS): remove "as unknown"
+      changeNum: (ctx.params[1] as unknown) as NumericChangeId,
+      patchNum: ctx.params[3] as PatchSetNum,
+      view: GerritView.CHANGE,
       edit: true,
     });
     this.reporting.setRepoName(project);
@@ -1475,7 +1634,7 @@
    * Normalize the patch range params for a the change or diff view and
    * redirect if URL upgrade is needed.
    */
-  _redirectOrNavigate(params) {
+  _redirectOrNavigate(params: GenerateUrlParameters & PatchRangeParams) {
     const needsRedirect = this._normalizePatchRangeParams(params);
     if (needsRedirect) {
       this._redirect(this._generateUrl(params));
@@ -1488,34 +1647,39 @@
     this._redirect('/settings/#Agreements');
   }
 
-  _handleNewAgreementsRoute(data) {
-    data.params.view = GerritNav.View.AGREEMENTS;
-    this._setParams(data.params);
+  _handleNewAgreementsRoute(data: PageContextWithQueryMap) {
+    data.params['view'] = GerritView.AGREEMENTS;
+    // TODO(TS): create valid object
+    this._setParams((data.params as unknown) as AppElementAgreementParam);
   }
 
-  _handleSettingsLegacyRoute(data) {
+  _handleSettingsLegacyRoute(data: PageContextWithQueryMap) {
     // email tokens may contain '+' but no space.
     // The parameter parsing replaces all '+' with a space,
     // undo that to have valid tokens.
     const token = data.params[0].replace(/ /g, '+');
     this._setParams({
-      view: GerritNav.View.SETTINGS,
+      view: GerritView.SETTINGS,
       emailToken: token,
     });
   }
 
-  _handleSettingsRoute(data) {
-    this._setParams({view: GerritNav.View.SETTINGS});
+  _handleSettingsRoute(_: PageContextWithQueryMap) {
+    this._setParams({view: GerritView.SETTINGS});
   }
 
-  _handleRegisterRoute(ctx) {
+  _handleRegisterRoute(ctx: PageContextWithQueryMap) {
     this._setParams({justRegistered: true});
     let path = ctx.params[0] || '/';
 
     // Prevent redirect looping.
-    if (path.startsWith('/register')) { path = '/'; }
+    if (path.startsWith('/register')) {
+      path = '/';
+    }
 
-    if (path[0] !== '/') { return; }
+    if (path[0] !== '/') {
+      return;
+    }
     this._redirect(getBaseUrl() + path);
   }
 
@@ -1531,32 +1695,35 @@
    * URL may sometimes have /+/ encoded to / /.
    * Context: Issue 6888, Issue 7100
    */
-  _handleImproperlyEncodedPlusRoute(ctx) {
+  _handleImproperlyEncodedPlusRoute(ctx: PageContextWithQueryMap) {
     let hash = this._getHashFromCanonicalPath(ctx.canonicalPath);
-    if (hash.length) { hash = '#' + hash; }
+    if (hash.length) {
+      hash = '#' + hash;
+    }
     this._redirect(`/c/${ctx.params[0]}/+/${ctx.params[1]}${hash}`);
   }
 
-  _handlePluginScreen(ctx) {
-    const view = GerritNav.View.PLUGIN_SCREEN;
+  _handlePluginScreen(ctx: PageContextWithQueryMap) {
+    const view = GerritView.PLUGIN_SCREEN;
     const plugin = ctx.params[0];
     const screen = ctx.params[1];
     this._setParams({view, plugin, screen});
   }
 
-  _handleDocumentationSearchRoute(data) {
+  _handleDocumentationSearchRoute(data: PageContextWithQueryMap) {
     this._setParams({
-      view: GerritNav.View.DOCUMENTATION_SEARCH,
-      filter: data.params.filter || null,
+      view: GerritView.DOCUMENTATION_SEARCH,
+      filter: data.params['filter'] || null,
     });
   }
 
-  _handleDocumentationSearchRedirectRoute(data) {
-    this._redirect('/Documentation/q/filter:' +
-        encodeURIComponent(data.params[0]));
+  _handleDocumentationSearchRedirectRoute(data: PageContextWithQueryMap) {
+    this._redirect(
+      '/Documentation/q/filter:' + encodeURIComponent(data.params[0])
+    );
   }
 
-  _handleDocumentationRedirectRoute(data) {
+  _handleDocumentationRedirectRoute(data: PageContextWithQueryMap) {
     if (data.params[1]) {
       location.reload();
     } else {
@@ -1582,9 +1749,14 @@
     // Note: the app's 404 display is tightly-coupled with catching 404
     // network responses, so we simulate a 404 response status to display it.
     // TODO: Decouple the gr-app error view from network responses.
-    this._appElement().dispatchEvent(new CustomEvent('page-error',
-        {detail: {response: {status: 404}}}));
+    this._appElement().dispatchEvent(
+      new CustomEvent('page-error', {detail: {response: {status: 404}}})
+    );
   }
 }
 
-customElements.define(GrRouter.is, GrRouter);
+declare global {
+  interface HTMLElementTagNameMap {
+    'gr-router': GrRouter;
+  }
+}