Migrate chrome://browser-switch to TypeScript.

Bug: 1189595
Change-Id: Ia5e235b41e7ca3094e2e0a24b4836052a22b5609
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2954438
Commit-Queue: dpapad <dpapad@chromium.org>
Reviewed-by: John Lee <johntlee@chromium.org>
Cr-Commit-Position: refs/heads/master@{#892021}
diff --git a/chrome/browser/resources/BUILD.gn b/chrome/browser/resources/BUILD.gn
index 8859049..4d0684a 100644
--- a/chrome/browser/resources/BUILD.gn
+++ b/chrome/browser/resources/BUILD.gn
@@ -176,9 +176,6 @@
         "webapks:closure_compile",
       ]
     }
-    if (is_win || is_mac || is_linux || is_chromeos_lacros) {
-      deps += [ "browser_switch:closure_compile" ]
-    }
 
     # Note: The following targets should only be type-checked on Windows builds,
     # but because Window bots don't depend on Java, |closure_compile| is off on
diff --git a/chrome/browser/resources/browser_switch/BUILD.gn b/chrome/browser/resources/browser_switch/BUILD.gn
index 60dd306..ddac49fe 100644
--- a/chrome/browser/resources/browser_switch/BUILD.gn
+++ b/chrome/browser/resources/browser_switch/BUILD.gn
@@ -3,11 +3,13 @@
 # found in the LICENSE file.
 
 import("//chrome/common/features.gni")
-import("//third_party/closure_compiler/compile_js.gni")
 import("//tools/grit/grit_rule.gni")
 import("//tools/polymer/html_to_js.gni")
+import("//tools/typescript/ts_library.gni")
 import("//ui/webui/resources/tools/generate_grd.gni")
 
+assert(is_win || is_mac || is_linux || is_chromeos_lacros)
+
 grit("resources") {
   defines = chrome_grit_defines
 
@@ -27,60 +29,48 @@
 
 generate_grd("build_grd") {
   grd_prefix = "browser_switch"
-  input_files = [
-    "app.js",
-    "browser_switch.html",
-    "browser_switch_proxy.js",
-    "internals/browser_switch_internals.html",
-    "internals/browser_switch_internals.js",
-  ]
-  input_files_base_dir = rebase_path(target_gen_dir, root_build_dir)
-
   out_grd = "$target_gen_dir/resources.grd"
+  input_files = [
+    "browser_switch.html",
+    "internals/browser_switch_internals.html",
+  ]
+  input_files_base_dir = rebase_path(".", "//")
+
+  deps = [ ":build_ts" ]
+  manifest_files = [ "$target_gen_dir/tsconfig.manifest" ]
+}
+
+ts_library("build_ts") {
+  root_dir = target_gen_dir
+  out_dir = "$target_gen_dir/tsc"
+  tsconfig_base = "tsconfig_base.json"
+  in_files = [
+    "app.ts",
+    "browser_switch_proxy.ts",
+    "internals/browser_switch_internals.ts",
+  ]
+  definitions = [ "//tools/typescript/definitions/chrome_send.d.ts" ]
   deps = [
-    ":copy_files",
-    ":copy_files_internals",
+    "//third_party/polymer/v3_0:library",
+    "//ui/webui/resources:library",
+  ]
+  extra_deps = [
+    ":copy_browser_proxy",
+    ":copy_internals",
     ":web_components",
   ]
 }
 
-copy("copy_files") {
-  sources = [
-    "browser_switch.html",
-    "browser_switch_proxy.js",
-  ]
+copy("copy_browser_proxy") {
+  sources = [ "browser_switch_proxy.ts" ]
   outputs = [ "$target_gen_dir/{{source_file_part}}" ]
 }
 
-copy("copy_files_internals") {
-  sources = [
-    "internals/browser_switch_internals.html",
-    "internals/browser_switch_internals.js",
-  ]
+copy("copy_internals") {
+  sources = [ "internals/browser_switch_internals.ts" ]
   outputs = [ "$target_gen_dir/internals/{{source_file_part}}" ]
 }
 
-js_type_check("closure_compile") {
-  is_polymer3 = true
-  deps = [
-    ":app",
-    ":browser_switch_proxy",
-    "internals:browser_switch_internals",
-  ]
-}
-
-js_library("app") {
-  deps = [
-    "//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
-    "//ui/webui/resources/js:i18n_behavior.m",
-    "//ui/webui/resources/js:load_time_data.m",
-  ]
-}
-
-js_library("browser_switch_proxy") {
-  deps = [ "//ui/webui/resources/js:cr.m" ]
-}
-
 html_to_js("web_components") {
-  js_files = [ "app.js" ]
+  js_files = [ "app.ts" ]
 }
diff --git a/chrome/browser/resources/browser_switch/app.js b/chrome/browser/resources/browser_switch/app.ts
similarity index 79%
rename from chrome/browser/resources/browser_switch/app.js
rename to chrome/browser/resources/browser_switch/app.ts
index 5cb79eb..4e70997 100644
--- a/chrome/browser/resources/browser_switch/app.js
+++ b/chrome/browser/resources/browser_switch/app.ts
@@ -7,30 +7,23 @@
 import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
 import './strings.m.js';
 
-import {I18nBehavior, I18nBehaviorInterface} from 'chrome://resources/js/i18n_behavior.m.js';
+import {I18nBehavior} from 'chrome://resources/js/i18n_behavior.m.js';
 import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
 import {html, mixinBehaviors, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
 import {BrowserSwitchProxy, BrowserSwitchProxyImpl} from './browser_switch_proxy.js';
 
-/** @type {number} */
-const MS_PER_SECOND = 1000;
+const MS_PER_SECOND: number = 1000;
 
-/** @enum {string} */
-const LaunchError = {
-  GENERIC_ERROR: 'genericError',
-  PROTOCOL_ERROR: 'protocolError',
-};
+enum LaunchError {
+  GENERIC_ERROR = 'genericError',
+  PROTOCOL_ERROR = 'protocolError',
+}
 
-/**
- * @constructor
- * @extends {PolymerElement}
- * @implements {I18nBehaviorInterface}
- */
 const BrowserSwitchAppElementBase =
-    mixinBehaviors([I18nBehavior], PolymerElement);
+    mixinBehaviors([I18nBehavior], PolymerElement) as
+    {new (): PolymerElement & I18nBehavior};
 
-/** @polymer */
 class BrowserSwitchAppElement extends BrowserSwitchAppElementBase {
   static get is() {
     return 'browser-switch-app';
@@ -44,7 +37,6 @@
     return {
       /**
        * URL to launch in the alternative browser.
-       * @private
        */
       url_: {
         type: String,
@@ -55,7 +47,6 @@
 
       /**
        * Error message, or empty string if no error has occurred (yet).
-       * @private
        */
       error_: {
         type: String,
@@ -65,7 +56,6 @@
       /**
        * Countdown displayed to the user, number of seconds until launching. If
        * 0 or less, doesn't get displayed at all.
-       * @private
        */
       secondCounter_: {
         type: Number,
@@ -74,6 +64,10 @@
     };
   }
 
+  private url_: string;
+  private error_: string;
+  private secondCounter_: number;
+
   /** @override */
   connectedCallback() {
     super.connectedCallback();
@@ -100,8 +94,7 @@
     this.startCountdown_(Math.floor(milliseconds / 1000));
   }
 
-  /** @private */
-  launchAndCloseTab_() {
+  private launchAndCloseTab_() {
     // Mark this page with '?done=...' so that restoring the tab doesn't
     // immediately re-trigger LBS.
     history.pushState({}, '', '/?done=true');
@@ -111,11 +104,7 @@
     });
   }
 
-  /**
-   * @param {number} seconds
-   * @private
-   */
-  startCountdown_(seconds) {
+  private startCountdown_(seconds: number) {
     this.secondCounter_ = seconds;
     const intervalId = setInterval(() => {
       this.secondCounter_--;
@@ -125,11 +114,7 @@
     }, 1 * MS_PER_SECOND);
   }
 
-  /**
-   * @return {string}
-   * @private
-   */
-  computeTitle_() {
+  private computeTitle_(): string {
     if (this.error_) {
       return this.i18n('errorTitle', getBrowserName());
     }
@@ -139,11 +124,7 @@
     return this.i18n('openingTitle', getBrowserName());
   }
 
-  /**
-   * @return {string}
-   * @private
-   */
-  computeDescription_() {
+  private computeDescription_(): string {
     if (this.error_) {
       return this.i18n(
           this.error_, getUrlHostname(this.url_), getBrowserName());
@@ -155,23 +136,17 @@
 
 customElements.define(BrowserSwitchAppElement.is, BrowserSwitchAppElement);
 
-/** @return {string} */
-function getBrowserName() {
+function getBrowserName(): string {
   return loadTimeData.getString('browserName');
 }
 
-/**
- * @param {string} url
- * @return {string}
- */
-function getUrlHostname(url) {
+function getUrlHostname(url: string): string {
   const anchor = document.createElement('a');
   anchor.href = url;
   // Return entire url if parsing failed (which means the URL is bogus).
   return anchor.hostname || url;
 }
 
-/** @return {!BrowserSwitchProxy} */
-function getProxy() {
+function getProxy(): BrowserSwitchProxy {
   return BrowserSwitchProxyImpl.getInstance();
 }
diff --git a/chrome/browser/resources/browser_switch/browser_switch_proxy.js b/chrome/browser/resources/browser_switch/browser_switch_proxy.js
deleted file mode 100644
index c9c5ddb..0000000
--- a/chrome/browser/resources/browser_switch/browser_switch_proxy.js
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright 2019 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import {sendWithPromise} from 'chrome://resources/js/cr.m.js';
-
-/** @interface */
-export class BrowserSwitchProxy {
-  /**
-   * @param {string} url URL to open in alternative browser.
-   * @return {Promise} A promise that can fail if unable to launch. It will
-   *     never resolve, because the tab closes if this succeeds.
-   */
-  launchAlternativeBrowserAndCloseTab(url) {}
-
-  gotoNewTabPage() {}
-}
-
-/** @implements {BrowserSwitchProxy} */
-export class BrowserSwitchProxyImpl {
-  /** @override */
-  launchAlternativeBrowserAndCloseTab(url) {
-    return sendWithPromise('launchAlternativeBrowserAndCloseTab', url);
-  }
-
-  /** @override */
-  gotoNewTabPage() {
-    chrome.send('gotoNewTabPage');
-  }
-
-  /** @return {!BrowserSwitchProxy} */
-  static getInstance() {
-    return instance || (instance = new BrowserSwitchProxyImpl());
-  }
-}
-
-/** @type {?BrowserSwitchProxy} */
-let instance = null;
diff --git a/chrome/browser/resources/browser_switch/browser_switch_proxy.ts b/chrome/browser/resources/browser_switch/browser_switch_proxy.ts
new file mode 100644
index 0000000..ff612cb0
--- /dev/null
+++ b/chrome/browser/resources/browser_switch/browser_switch_proxy.ts
@@ -0,0 +1,33 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {sendWithPromise} from 'chrome://resources/js/cr.m.js';
+
+/** @interface */
+export interface BrowserSwitchProxy {
+  /**
+   * @param URL to open in alternative browser.
+   * @return A promise that can fail if unable to launch. It will never resolve,
+   *     because the tab closes if this succeeds.
+   */
+  launchAlternativeBrowserAndCloseTab(url: string): Promise<void>;
+
+  gotoNewTabPage(): void;
+}
+
+export class BrowserSwitchProxyImpl implements BrowserSwitchProxy {
+  launchAlternativeBrowserAndCloseTab(url: string) {
+    return sendWithPromise('launchAlternativeBrowserAndCloseTab', url);
+  }
+
+  gotoNewTabPage() {
+    chrome.send('gotoNewTabPage');
+  }
+
+  static getInstance(): BrowserSwitchProxy {
+    return instance || (instance = new BrowserSwitchProxyImpl());
+  }
+}
+
+let instance: BrowserSwitchProxy|null = null;
diff --git a/chrome/browser/resources/browser_switch/internals/browser_switch_internals.js b/chrome/browser/resources/browser_switch/internals/browser_switch_internals.ts
similarity index 68%
rename from chrome/browser/resources/browser_switch/internals/browser_switch_internals.js
rename to chrome/browser/resources/browser_switch/internals/browser_switch_internals.ts
index b79d221..7c899e3 100644
--- a/chrome/browser/resources/browser_switch/internals/browser_switch_internals.js
+++ b/chrome/browser/resources/browser_switch/internals/browser_switch_internals.ts
@@ -5,46 +5,36 @@
 import {sendWithPromise} from 'chrome://resources/js/cr.m.js';
 import {$} from 'chrome://resources/js/util.m.js';
 
-/**
- * @typedef {{
- *   sitelist: Array<string>,
- *   greylist: Array<string>,
- * }}
- */
-let RuleSet;
+type RuleSet = {
+  sitelist: Array<string>;
+  greylist: Array<string>;
+};
 
-/**
- * @typedef {{
- *   gpo: RuleSet,
- *   ieem: (RuleSet|undefined),
- *   external: (RuleSet|undefined),
- * }}
- */
-let RuleSetList;
+type RuleSetList = {
+  gpo: RuleSet;
+  ieem?: RuleSet;
+  external?: RuleSet;
+};
 
 /**
  * Returned by getRulesetSources().
- * @typedef {{
- *   browser_switcher: Object<string, string>!,
- * }}
  */
-let RulesetSources;
+type RulesetSources = {
+  browser_switcher: {[k: string]: string};
+};
 
 /**
  * Returned by getTimestamps().
- * @typedef {{
- *   last_fetch: number,
- *   next_fetch: number,
- * }}
  */
-let TimestampPair;
+type TimestampPair = {
+  last_fetch: number;
+  next_fetch: number;
+};
 
 /**
  * Converts 'this_word' to 'ThisWord'
- * @param {string} symbol
- * @return {string}
  */
-function snakeCaseToUpperCamelCase(symbol) {
+function snakeCaseToUpperCamelCase(symbol: string): string {
   if (!symbol) {
     return symbol;
   }
@@ -55,21 +45,19 @@
 
 /**
  * Clears the table, and inserts a header row.
- * @param {HTMLTableElement} table
- * @param {HTMLTemplateElement} headerTemplate
- *     Template to use to re-create the header row.
+ * @param headerTemplate Template to use to re-create the header row.
  */
-function clearTable(table, headerTemplate) {
+function clearTable(
+    table: HTMLTableElement, headerTemplate: HTMLTemplateElement) {
   table.innerHTML = '';
   const headerRow = document.importNode(headerTemplate.content, true);
   table.appendChild(headerRow);
 }
 
 /**
- * @param {string} rule
- * @return {string} String describing the rule type.
+ * @return String describing the rule type.
  */
-function getRuleType(rule) {
+function getRuleType(rule: string): string {
   if (rule == '*') {
     return 'wildcard';
   }
@@ -81,33 +69,31 @@
 
 /**
  * Creates and returns a <tr> element for the given rule.
- * @param {string} rule
- * @param {string} rulesetName
- * @return {HTMLTableRowElement}
  */
-function createRowForRule(rule, rulesetName) {
-  const row = document.importNode($('rule-row-template').content, true);
+function createRowForRule(
+    rule: string, rulesetName: string): HTMLTableRowElement {
+  const row = document.importNode(
+                  ($('rule-row-template') as HTMLTemplateElement).content,
+                  true) as unknown as HTMLTableRowElement;
   const cells = row.querySelectorAll('td');
   cells[0].innerText = rule;
   cells[0].className = 'url';
   cells[1].innerText = rulesetName;
   cells[2].innerText = getRuleType(rule);
   cells[3].innerText = /^!/.test(rule) ? 'yes' : 'no';
-  return /** @type {HTMLTableRowElement} */ (row);
+  return row;
 }
 
 /**
  * Updates the content of all tables after receiving data from the backend.
- * @param {RuleSetList} rulesets
  */
-function updateTables(rulesets) {
-  const headerTemplate =
-      /** @type {HTMLTemplateElement} */ ($('header-row-template'));
-  clearTable(/** @type {HTMLTableElement} */ ($('sitelist')), headerTemplate);
-  clearTable(/** @type {HTMLTableElement} */ ($('greylist')), headerTemplate);
+function updateTables(rulesets: RuleSetList) {
+  const headerTemplate = $('header-row-template') as HTMLTemplateElement;
+  clearTable($('sitelist') as HTMLTableElement, headerTemplate);
+  clearTable($('greylist') as HTMLTableElement, headerTemplate);
 
   for (const [rulesetName, ruleset] of Object.entries(rulesets)) {
-    for (const [listName, rules] of Object.entries(ruleset)) {
+    for (const [listName, rules] of Object.entries(ruleset as RuleSet)) {
       const table = $(listName);
       for (const rule of rules) {
         table.appendChild(createRowForRule(rule, rulesetName));
@@ -117,7 +103,7 @@
 }
 
 function checkUrl() {
-  const url = $('url-checker-input').value;
+  const url = ($('url-checker-input') as HTMLInputElement).value;
   if (!url) {
     $('output').innerText = '';
     return;
@@ -127,7 +113,7 @@
         // URL is valid.
         $('output').innerText = JSON.stringify(decision, null, 2);
       })
-      .catch(err => {
+      .catch(() => {
         // URL is invalid.
         $('output').innerText =
             'Invalid URL. Make sure it is formatted properly.';
@@ -138,10 +124,8 @@
 
 /**
  * Formats |date| as "HH:MM:SS".
- * @param {Date} date
- * @return {string}
  */
-function formatTime(date) {
+function formatTime(date: Date): string {
   const hh = date.getHours().toString().padStart(2, '0');
   const mm = date.getMinutes().toString().padStart(2, '0');
   const ss = date.getSeconds().toString().padStart(2, '0');
@@ -150,9 +134,8 @@
 
 /**
  * Update the paragraphs under the "XML sitelists" section.
- * @param {TimestampPair?} timestamps
  */
-function updateTimestamps(timestamps) {
+function updateTimestamps(timestamps: TimestampPair|null) {
   if (!timestamps) {
     return;
   }
@@ -178,20 +161,18 @@
 
 /**
  * Update the table under the "XML sitelists" section.
- * @param {RulesetSources} sources
  */
-function updateXmlTable({browser_switcher: sources}) {
-  const headerTemplate =
-      /** @type {HTMLTemplateElement} */ ($('xml-header-row-template'));
-  clearTable(
-      /** @type {HTMLTableElement} */ ($('xml-sitelists')), headerTemplate);
+function updateXmlTable({browser_switcher: sources}: RulesetSources) {
+  const headerTemplate = $('xml-header-row-template') as HTMLTemplateElement;
+  clearTable($('xml-sitelists') as HTMLTableElement, headerTemplate);
 
   for (const [prefName, url] of Object.entries(sources)) {
     // Hacky: guess the policy name from the pref name by converting 'foo_bar'
     // to 'BrowserSwitcherFooBar'. This relies on prefs having the same name as
     // the associated policy.
     const policyName = 'BrowserSwitcher' + snakeCaseToUpperCamelCase(prefName);
-    const row = document.importNode($('xml-row-template').content, true);
+    const row = document.importNode(
+        ($('xml-row-template') as HTMLTemplateElement).content, true);
     const cells = row.querySelectorAll('td');
     cells[0].innerText = policyName;
     cells[1].innerText = url || '(not configured)';
diff --git a/chrome/browser/resources/browser_switch/tsconfig_base.json b/chrome/browser/resources/browser_switch/tsconfig_base.json
new file mode 100644
index 0000000..afa07315
--- /dev/null
+++ b/chrome/browser/resources/browser_switch/tsconfig_base.json
@@ -0,0 +1,8 @@
+{
+  "extends": "../../../../tools/typescript/tsconfig_base.json",
+  "compilerOptions": {
+    "noUncheckedIndexedAccess": false,
+    "noUnusedLocals": false,
+    "strictPropertyInitialization": false
+  }
+}