Convert files to typescript

The change converts the following files to typescript:

* elements/diff/gr-diff-builder/gr-diff-builder-element.ts

Change-Id: Ic28adbe27eb2707427911cc1f956ed312689b160
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.ts
index ef75da0..09e0df8 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.ts
@@ -14,23 +14,45 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../gr-coverage-layer/gr-coverage-layer.js';
-import '../gr-diff-processor/gr-diff-processor.js';
-import '../../shared/gr-hovercard/gr-hovercard.js';
-import '../gr-ranged-comment-layer/gr-ranged-comment-layer.js';
-import './gr-diff-builder-side-by-side.js';
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.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 {htmlTemplate} from './gr-diff-builder-element_html.js';
-import {GrAnnotation} from '../gr-diff-highlight/gr-annotation.js';
-import {GrDiffBuilder} from './gr-diff-builder.js';
-import {GrDiffBuilderSideBySide} from './gr-diff-builder-side-by-side.js';
-import {GrDiffBuilderImage} from './gr-diff-builder-image.js';
-import {GrDiffBuilderUnified} from './gr-diff-builder-unified.js';
-import {GrDiffBuilderBinary} from './gr-diff-builder-binary.js';
-import {util} from '../../../scripts/util.js';
+import '../gr-coverage-layer/gr-coverage-layer';
+import '../gr-diff-processor/gr-diff-processor';
+import '../../shared/gr-hovercard/gr-hovercard';
+import '../gr-ranged-comment-layer/gr-ranged-comment-layer';
+import './gr-diff-builder-side-by-side';
+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 {htmlTemplate} from './gr-diff-builder-element_html';
+import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
+import {GrDiffBuilder} from './gr-diff-builder';
+import {GrDiffBuilderSideBySide} from './gr-diff-builder-side-by-side';
+import {GrDiffBuilderImage} from './gr-diff-builder-image';
+import {GrDiffBuilderUnified} from './gr-diff-builder-unified';
+import {GrDiffBuilderBinary} from './gr-diff-builder-binary';
+import {CancelablePromise, util} from '../../../scripts/util';
+import {customElement, property, observe} from '@polymer/decorators';
+import {
+  BlameInfo,
+  DiffInfo,
+  DiffPreferencesInfo,
+  ImageInfo,
+} from '../../../types/common';
+import {CoverageRange} from '../../../types/types';
+import {
+  GrDiffProcessor,
+  KeyLocations,
+} from '../gr-diff-processor/gr-diff-processor';
+import {
+  CommentRangeLayer,
+  DiffLayer,
+  GrRangedCommentLayer,
+} from '../gr-ranged-comment-layer/gr-ranged-comment-layer';
+import {GrCoverageLayer} from '../gr-coverage-layer/gr-coverage-layer';
+import {Side} from '../../../constants/constants';
+import {GrDiffLine, LineNumber} from '../gr-diff/gr-diff-line';
+import {GrDiffGroup} from '../gr-diff/gr-diff-group';
+import {PolymerSpliceChange} from '@polymer/polymer/interfaces';
+import {getLineNumber} from '../gr-diff/gr-diff-utils';
 
 const DiffViewMode = {
   SIDE_BY_SIDE: 'SIDE_BY_SIDE',
@@ -43,14 +65,23 @@
 const COMMIT_MSG_PATH = '/COMMIT_MSG';
 const COMMIT_MSG_LINE_LENGTH = 72;
 
-/**
- * @extends PolymerElement
- */
-class GrDiffBuilderElement extends GestureEventListeners(
-    LegacyElementMixin(PolymerElement)) {
-  static get template() { return htmlTemplate; }
+export interface GrDiffBuilderElement {
+  $: {
+    processor: GrDiffProcessor;
+    rangeLayer: GrRangedCommentLayer;
+    coverageLayerLeft: GrCoverageLayer;
+    coverageLayerRight: GrCoverageLayer;
+  };
+}
 
-  static get is() { return 'gr-diff-builder'; }
+@customElement('gr-diff-builder')
+export class GrDiffBuilderElement extends GestureEventListeners(
+  LegacyElementMixin(PolymerElement)
+) {
+  static get template() {
+    return htmlTemplate;
+  }
+
   /**
    * Fired when the diff begins rendering.
    *
@@ -63,55 +94,85 @@
    * @event render-content
    */
 
-  static get properties() {
-    return {
-      diff: Object,
-      changeNum: String,
-      patchNum: String,
-      viewMode: String,
-      isImageDiff: Boolean,
-      baseImage: Object,
-      revisionImage: Object,
-      parentIndex: Number,
-      path: String,
-      projectName: String,
+  @property({type: Object})
+  diff?: DiffInfo;
 
-      _builder: Object,
-      _groups: Array,
-      _layers: Array,
-      _showTabs: Boolean,
-      /** @type {!Array<!Gerrit.HoveredRange>} */
-      commentRanges: {
-        type: Array,
-        value: () => [],
-      },
-      /** @type {!Array<!Gerrit.CoverageRange>} */
-      coverageRanges: {
-        type: Array,
-        value: () => [],
-      },
-      _leftCoverageRanges: {
-        type: Array,
-        computed: '_computeLeftCoverageRanges(coverageRanges)',
-      },
-      _rightCoverageRanges: {
-        type: Array,
-        computed: '_computeRightCoverageRanges(coverageRanges)',
-      },
-      /**
-       * The promise last returned from `render()` while the asynchronous
-       * rendering is running - `null` otherwise. Provides a `cancel()`
-       * method that rejects it with `{isCancelled: true}`.
-       *
-       * @type {?Object}
-       */
-      _cancelableRenderPromise: Object,
-      layers: {
-        type: Array,
-        value: [],
-      },
-    };
-  }
+  @property({type: String})
+  changeNum?: string;
+
+  @property({type: String})
+  patchNum?: string;
+
+  @property({type: String})
+  viewMode?: string;
+
+  @property({type: Boolean})
+  isImageDiff?: boolean;
+
+  @property({type: Object})
+  baseImage: ImageInfo | null = null;
+
+  @property({type: Object})
+  revisionImage: ImageInfo | null = null;
+
+  @property({type: Number})
+  parentIndex?: number;
+
+  @property({type: String})
+  path?: string;
+
+  @property({type: String})
+  projectName?: string;
+
+  @property({type: Object})
+  _builder?: GrDiffBuilder;
+
+  @property({type: Array})
+  _groups: GrDiffGroup[] = [];
+
+  /**
+   * Layers passed in from the outside.
+   */
+  @property({type: Array})
+  layers: DiffLayer[] = [];
+
+  /**
+   * All layers, both from the outside and the default ones.
+   */
+  @property({type: Array})
+  _layers: DiffLayer[] = [];
+
+  @property({type: Boolean})
+  _showTabs?: boolean;
+
+  @property({type: Boolean})
+  _showTrailingWhitespace?: boolean;
+
+  @property({type: Array})
+  commentRanges: CommentRangeLayer[] = [];
+
+  @property({type: Array})
+  coverageRanges: CoverageRange[] = [];
+
+  @property({
+    type: Array,
+    computed: '_computeLeftCoverageRanges(coverageRanges)',
+  })
+  _leftCoverageRanges?: CoverageRange[];
+
+  @property({
+    type: Array,
+    computed: '_computeRightCoverageRanges(coverageRanges)',
+  })
+  _rightCoverageRanges?: CoverageRange[];
+
+  /**
+   * The promise last returned from `render()` while the asynchronous
+   * rendering is running - `null` otherwise. Provides a `cancel()`
+   * method that rejects it with `{isCancelled: true}`.
+   */
+  @property({type: Object})
+  _cancelableRenderPromise: CancelablePromise<unknown> | null = null;
 
   /** @override */
   detached() {
@@ -122,24 +183,18 @@
   }
 
   get diffElement() {
-    return this.queryEffectiveChildren('#diffTable');
+    return this.queryEffectiveChildren('#diffTable') as HTMLElement;
   }
 
-  static get observers() {
-    return [
-      '_groupsChanged(_groups.splices)',
-    ];
-  }
-
-  _computeLeftCoverageRanges(coverageRanges) {
+  _computeLeftCoverageRanges(coverageRanges: CoverageRange[]) {
     return coverageRanges.filter(range => range && range.side === 'left');
   }
 
-  _computeRightCoverageRanges(coverageRanges) {
+  _computeRightCoverageRanges(coverageRanges: CoverageRange[]) {
     return coverageRanges.filter(range => range && range.side === 'right');
   }
 
-  render(keyLocations, prefs) {
+  render(keyLocations: KeyLocations, prefs: DiffPreferencesInfo) {
     // Setting up annotation layers must happen after plugins are
     // installed, and |render| satisfies the requirement, however,
     // |attached| doesn't because in the diff view page, the element is
@@ -155,6 +210,9 @@
     if (this._builder) {
       this._builder.clear();
     }
+    if (!this.diff) {
+      throw Error('Cannot render a diff without DiffInfo.');
+    }
     this._builder = this._getDiffBuilder(this.diff, prefs);
 
     this.$.processor.context = prefs.context;
@@ -165,23 +223,32 @@
 
     const isBinary = !!(this.isImageDiff || this.diff.binary);
 
-    this.dispatchEvent(new CustomEvent(
-        'render-start', {bubbles: true, composed: true}));
+    this.dispatchEvent(
+      new CustomEvent('render-start', {bubbles: true, composed: true})
+    );
     this._cancelableRenderPromise = util.makeCancelable(
-        this.$.processor.process(this.diff.content, isBinary)
-            .then(() => {
-              if (this.isImageDiff) {
-                this._builder.renderDiff();
-              }
-              this.dispatchEvent(new CustomEvent('render-content',
-                  {bubbles: true, composed: true}));
-            }));
-    return this._cancelableRenderPromise
-        .finally(() => { this._cancelableRenderPromise = null; })
-    // Mocca testing does not like uncaught rejections, so we catch
-    // the cancels which are expected and should not throw errors in
-    // tests.
-        .catch(e => { if (!e.isCanceled) return Promise.reject(e); });
+      this.$.processor.process(this.diff.content, isBinary).then(() => {
+        if (this.isImageDiff) {
+          (this._builder as GrDiffBuilderImage).renderDiff();
+        }
+        this.dispatchEvent(
+          new CustomEvent('render-content', {bubbles: true, composed: true})
+        );
+      })
+    );
+    return (
+      this._cancelableRenderPromise
+        .finally(() => {
+          this._cancelableRenderPromise = null;
+        })
+        // Mocca testing does not like uncaught rejections, so we catch
+        // the cancels which are expected and should not throw errors in
+        // tests.
+        .catch(e => {
+          if (!e.isCanceled) return Promise.reject(e);
+          return;
+        })
+    );
   }
 
   _setupAnnotationLayers() {
@@ -200,87 +267,91 @@
     this._layers = layers;
   }
 
-  getLineElByChild(node) {
+  getLineElByChild(node?: Node): HTMLElement | null {
     while (node) {
       if (node instanceof Element) {
         if (node.classList.contains('lineNum')) {
-          return node;
+          return node as HTMLElement;
         }
         if (node.classList.contains('section')) {
           return null;
         }
       }
-      node = node.previousSibling || node.parentElement;
+      node = node.previousSibling ?? node.parentElement ?? undefined;
     }
     return null;
   }
 
-  getLineNumberByChild(node) {
+  getLineNumberByChild(node: Node) {
     const lineEl = this.getLineElByChild(node);
-    return lineEl ?
-      parseInt(lineEl.getAttribute('data-value'), 10) :
-      null;
+    return getLineNumber(lineEl);
   }
 
-  getContentTdByLine(lineNumber, opt_side, opt_root) {
-    return this._builder.getContentTdByLine(lineNumber, opt_side, opt_root);
+  getContentTdByLine(lineNumber: LineNumber, side?: Side, root?: Element) {
+    if (!this._builder) return null;
+    return this._builder.getContentTdByLine(lineNumber, side, root);
   }
 
-  _getDiffRowByChild(child) {
+  _getDiffRowByChild(child: Element) {
     while (!child.classList.contains('diff-row') && child.parentElement) {
       child = child.parentElement;
     }
     return child;
   }
 
-  getContentTdByLineEl(lineEl) {
-    if (!lineEl) return;
-    const line = lineEl.getAttribute('data-value');
+  getContentTdByLineEl(lineEl?: Element): Element | null {
+    if (!lineEl) return null;
+    const line = getLineNumber(lineEl);
+    if (!line) return null;
     const side = this.getSideByLineEl(lineEl);
     // Performance optimization because we already have an element in the
     // correct row
-    const row = dom(this._getDiffRowByChild(lineEl));
+    const row = this._getDiffRowByChild(lineEl);
     return this.getContentTdByLine(line, side, row);
   }
 
-  getLineElByNumber(lineNumber, opt_side) {
-    const sideSelector = opt_side ? ('.' + opt_side) : '';
+  getLineElByNumber(lineNumber: string, side?: Side) {
+    const sideSelector = side ? '.' + side : '';
     return this.diffElement.querySelector(
-        '.lineNum[data-value="' + lineNumber + '"]' + sideSelector);
+      '.lineNum[data-value="' + lineNumber + '"]' + sideSelector
+    );
   }
 
-  getContentsByLineRange(startLine, endLine, opt_side) {
-    const result = [];
-    this._builder.findLinesByRange(startLine, endLine, opt_side, null,
-        result);
-    return result;
+  getSideByLineEl(lineEl: Element) {
+    return lineEl.classList.contains(GrDiffBuilder.Side.RIGHT)
+      ? Side.RIGHT
+      : Side.LEFT;
   }
 
-  getSideByLineEl(lineEl) {
-    return lineEl.classList.contains(GrDiffBuilder.Side.RIGHT) ?
-      GrDiffBuilder.Side.RIGHT : GrDiffBuilder.Side.LEFT;
-  }
-
-  emitGroup(group, sectionEl) {
+  emitGroup(group: GrDiffGroup, sectionEl: HTMLElement) {
+    if (!this._builder) return;
     this._builder.emitGroup(group, sectionEl);
   }
 
-  showContext(newGroups, sectionEl) {
+  showContext(newGroups: GrDiffGroup[], sectionEl: HTMLElement) {
+    if (!this._builder) return;
     const groups = this._builder.groups;
 
-    const contextIndex = groups.findIndex(group =>
-      group.element === sectionEl
-    );
+    const contextIndex = groups.findIndex(group => group.element === sectionEl);
     groups.splice(contextIndex, 1, ...newGroups);
 
     for (const newGroup of newGroups) {
       this._builder.emitGroup(newGroup, sectionEl);
     }
-    sectionEl.parentNode.removeChild(sectionEl);
+    if (sectionEl.parentNode) {
+      sectionEl.parentNode.removeChild(sectionEl);
+    }
 
-    this.async(() => this.dispatchEvent(new CustomEvent('render-content', {
-      composed: true, bubbles: true,
-    })), 1);
+    this.async(
+      () =>
+        this.dispatchEvent(
+          new CustomEvent('render-content', {
+            composed: true,
+            bubbles: true,
+          })
+        ),
+      1
+    );
   }
 
   cancel() {
@@ -291,25 +362,29 @@
     }
   }
 
-  _handlePreferenceError(pref) {
-    const message = `The value of the '${pref}' user preference is ` +
-        `invalid. Fix in diff preferences`;
-    this.dispatchEvent(new CustomEvent('show-alert', {
-      detail: {
-        message,
-      }, bubbles: true, composed: true}));
+  _handlePreferenceError(pref: string): never {
+    const message =
+      `The value of the '${pref}' user preference is ` +
+      'invalid. Fix in diff preferences';
+    this.dispatchEvent(
+      new CustomEvent('show-alert', {
+        detail: {
+          message,
+        },
+        bubbles: true,
+        composed: true,
+      })
+    );
     throw Error(`Invalid preference value: ${pref}`);
   }
 
-  _getDiffBuilder(diff, prefs) {
+  _getDiffBuilder(diff: DiffInfo, prefs: DiffPreferencesInfo): GrDiffBuilder {
     if (isNaN(prefs.tab_size) || prefs.tab_size <= 0) {
       this._handlePreferenceError('tab size');
-      return;
     }
 
     if (isNaN(prefs.line_length) || prefs.line_length <= 0) {
       this._handlePreferenceError('diff width');
-      return;
     }
 
     const localPrefs = {...prefs};
@@ -322,49 +397,51 @@
     let builder = null;
     if (this.isImageDiff) {
       builder = new GrDiffBuilderImage(
-          diff,
-          localPrefs,
-          this.diffElement,
-          this.baseImage,
-          this.revisionImage);
+        diff,
+        localPrefs,
+        this.diffElement,
+        this.baseImage,
+        this.revisionImage
+      );
     } else if (diff.binary) {
       // If the diff is binary, but not an image.
-      return new GrDiffBuilderBinary(
-          diff,
-          localPrefs,
-          this.diffElement);
+      return new GrDiffBuilderBinary(diff, localPrefs, this.diffElement);
     } else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
       builder = new GrDiffBuilderSideBySide(
-          diff,
-          localPrefs,
-          this.diffElement,
-          this._layers
+        diff,
+        localPrefs,
+        this.diffElement,
+        this._layers
       );
     } else if (this.viewMode === DiffViewMode.UNIFIED) {
       builder = new GrDiffBuilderUnified(
-          diff,
-          localPrefs,
-          this.diffElement,
-          this._layers);
+        diff,
+        localPrefs,
+        this.diffElement,
+        this._layers
+      );
     }
     if (!builder) {
-      throw Error('Unsupported diff view mode: ' + this.viewMode);
+      throw Error(`Unsupported diff view mode: ${this.viewMode}`);
     }
     return builder;
   }
 
   _clearDiffContent() {
-    this.diffElement.innerHTML = null;
+    this.diffElement.innerHTML = '';
   }
 
-  _groupsChanged(changeRecord) {
-    if (!changeRecord) { return; }
+  @observe('_groups.splices')
+  _groupsChanged(changeRecord: PolymerSpliceChange<GrDiffGroup[]>) {
+    if (!changeRecord || !this._builder) {
+      return;
+    }
     for (const splice of changeRecord.indexSplices) {
       let group;
       for (let i = 0; i < splice.addedCount; i++) {
         group = splice.object[splice.index + i];
         this._builder.groups.push(group);
-        this._builder.emitGroup(group);
+        this._builder.emitGroup(group, null);
       }
     }
   }
@@ -374,24 +451,28 @@
       // Take a DIV.contentText element and a line object with intraline
       // differences to highlight and apply them to the element as
       // annotations.
-      annotate(contentEl, lineNumberEl, line) {
+      annotate(contentEl: HTMLElement, _: HTMLElement, line: GrDiffLine) {
         const HL_CLASS = 'style-scope gr-diff intraline';
         for (const highlight of line.highlights) {
           // The start and end indices could be the same if a highlight is
           // meant to start at the end of a line and continue onto the
           // next one. Ignore it.
-          if (highlight.startIndex === highlight.endIndex) { continue; }
+          if (highlight.startIndex === highlight.endIndex) {
+            continue;
+          }
 
           // If endIndex isn't present, continue to the end of the line.
-          const endIndex = highlight.endIndex === undefined ?
-            line.text.length :
-            highlight.endIndex;
+          const endIndex =
+            highlight.endIndex === undefined
+              ? line.text.length
+              : highlight.endIndex;
 
           GrAnnotation.annotateElement(
-              contentEl,
-              highlight.startIndex,
-              endIndex - highlight.startIndex,
-              HL_CLASS);
+            contentEl,
+            highlight.startIndex,
+            endIndex - highlight.startIndex,
+            HL_CLASS
+          );
         }
       },
     };
@@ -400,19 +481,27 @@
   _createTabIndicatorLayer() {
     const show = () => this._showTabs;
     return {
-      annotate(contentEl, lineNumberEl, line) {
+      annotate(contentEl: HTMLElement, _: HTMLElement, line: GrDiffLine) {
         // If visible tabs are disabled, do nothing.
-        if (!show()) { return; }
+        if (!show()) {
+          return;
+        }
 
         // Find and annotate the locations of tabs.
         const split = line.text.split('\t');
-        if (!split) { return; }
+        if (!split) {
+          return;
+        }
         for (let i = 0, pos = 0; i < split.length - 1; i++) {
           // Skip forward by the length of the content
           pos += split[i].length;
 
-          GrAnnotation.annotateElement(contentEl, pos, 1,
-              'style-scope gr-diff tab-indicator');
+          GrAnnotation.annotateElement(
+            contentEl,
+            pos,
+            1,
+            'style-scope gr-diff tab-indicator'
+          );
 
           // Skip forward by one tab character.
           pos++;
@@ -422,32 +511,45 @@
   }
 
   _createTrailingWhitespaceLayer() {
-    const show = function() {
+    const show = () => {
       return this._showTrailingWhitespace;
-    }.bind(this);
+    };
 
     return {
-      annotate(contentEl, lineNumberEl, line) {
-        if (!show()) { return; }
+      annotate(contentEl: HTMLElement, _: HTMLElement, line: GrDiffLine) {
+        if (!show()) {
+          return;
+        }
 
         const match = line.text.match(TRAILING_WHITESPACE_PATTERN);
         if (match) {
           // Normalize string positions in case there is unicode before or
           // within the match.
           const index = GrAnnotation.getStringLength(
-              line.text.substr(0, match.index));
+            line.text.substr(0, match.index)
+          );
           const length = GrAnnotation.getStringLength(match[0]);
-          GrAnnotation.annotateElement(contentEl, index, length,
-              'style-scope gr-diff trailing-whitespace');
+          GrAnnotation.annotateElement(
+            contentEl,
+            index,
+            length,
+            'style-scope gr-diff trailing-whitespace'
+          );
         }
       },
     };
   }
 
-  setBlame(blame) {
-    if (!this._builder || !blame) { return; }
+  setBlame(blame: BlameInfo[]) {
+    if (!this._builder || !blame) {
+      return;
+    }
     this._builder.setBlame(blame);
   }
 }
 
-customElements.define(GrDiffBuilderElement.is, GrDiffBuilderElement);
+declare global {
+  interface HTMLElementTagNameMap {
+    'gr-diff-builder': GrDiffBuilderElement;
+  }
+}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js
index 9eafda2..7cbbdb9 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js
@@ -296,18 +296,15 @@
     }
   });
 
-  test('_handlePreferenceError called with invalid preference', () => {
-    sinon.stub(element, '_handlePreferenceError');
+  test('_handlePreferenceError throws with invalid preference', () => {
     const prefs = {tab_size: 0};
-    element._getDiffBuilder(element.diff, prefs);
-    assert.isTrue(element._handlePreferenceError.lastCall
-        .calledWithExactly('tab size'));
+    assert.throws(() => element._getDiffBuilder(element.diff, prefs));
   });
 
   test('_handlePreferenceError triggers alert and javascript error', () => {
     const errorStub = sinon.stub();
     element.addEventListener('show-alert', errorStub);
-    assert.throws(element._handlePreferenceError.bind(element, 'tab size'));
+    assert.throws(() => element._handlePreferenceError('tab size'));
     assert.equal(errorStub.lastCall.args[0].detail.message,
         `The value of the 'tab size' user preference is invalid. ` +
       `Fix in diff preferences`);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.ts
index 43086ae..f3de371 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.ts
@@ -17,6 +17,7 @@
 
 import {GrDiffBuilderSideBySide} from './gr-diff-builder-side-by-side';
 import {DiffInfo, DiffPreferencesInfo, ImageInfo} from '../../../types/common';
+import {GrEndpointParam} from '../../plugins/gr-endpoint-param/gr-endpoint-param';
 
 // MIME types for images we allow showing. Do not include SVG, it can contain
 // arbitrary JavaScript.
@@ -27,8 +28,8 @@
     diff: DiffInfo,
     prefs: DiffPreferencesInfo,
     outputEl: HTMLElement,
-    private readonly _baseImage: ImageInfo,
-    private readonly _revisionImage: ImageInfo
+    private readonly _baseImage: ImageInfo | null,
+    private readonly _revisionImage: ImageInfo | null
   ) {
     super(diff, prefs, outputEl, []);
   }
@@ -66,10 +67,11 @@
     return tbody;
   }
 
-  private _createEndpointParam(name: string, value: ImageInfo) {
-    // TODO(TS): Replace any by GrEndpointParam type when it is available.
-    const endpointParam: any = this._createElement('gr-endpoint-param');
-    endpointParam.setAttribute('name', name);
+  private _createEndpointParam(name: string, value: ImageInfo | null) {
+    const endpointParam = this._createElement(
+      'gr-endpoint-param'
+    ) as GrEndpointParam;
+    endpointParam.name = name;
     endpointParam.value = value;
     return endpointParam;
   }
@@ -89,7 +91,7 @@
   }
 
   private _createImageCell(
-    image: ImageInfo,
+    image: ImageInfo | null,
     className: string,
     section: HTMLElement
   ) {
@@ -122,7 +124,7 @@
     this._setLabelText(label, image);
   }
 
-  private _setLabelText(label: HTMLElement, image: ImageInfo) {
+  private _setLabelText(label: HTMLElement, image: ImageInfo | null) {
     label.textContent = _getImageLabel(image);
   }
 
@@ -147,7 +149,7 @@
 
     if (addNamesInLabel) {
       nameSpan = this._createElement('span', 'name');
-      nameSpan.textContent = this._baseImage._name;
+      nameSpan.textContent = this._baseImage?._name ?? '';
       label.appendChild(nameSpan);
       label.appendChild(this._createElement('br'));
     }
@@ -165,7 +167,7 @@
 
     if (addNamesInLabel) {
       nameSpan = this._createElement('span', 'name');
-      nameSpan.textContent = this._revisionImage._name;
+      nameSpan.textContent = this._revisionImage?._name ?? '';
       label.appendChild(nameSpan);
       label.appendChild(this._createElement('br'));
     }
@@ -180,9 +182,9 @@
   }
 }
 
-function _getImageLabel(image: ImageInfo) {
+function _getImageLabel(image: ImageInfo | null) {
   if (image) {
-    const type = image.type || image._expectedType;
+    const type = image.type ?? image._expectedType;
     if (image._width && image._height) {
       return `${image._width}×${image._height} ${type}`;
     } else {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
index d98f134..9ebf4b9 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
@@ -72,7 +72,7 @@
 
   protected readonly _outputEl: HTMLElement;
 
-  private readonly groups: GrDiffGroup[];
+  readonly groups: GrDiffGroup[];
 
   private _blameInfo: BlameInfo[] | null;
 
@@ -155,7 +155,7 @@
 
   abstract buildSectionElement(group: GrDiffGroup): HTMLElement;
 
-  emitGroup(group: GrDiffGroup, beforeSection: HTMLElement) {
+  emitGroup(group: GrDiffGroup, beforeSection: HTMLElement | null) {
     const element = this.buildSectionElement(group);
     this._outputEl.insertBefore(element, beforeSection);
     group.element = element;
@@ -198,7 +198,7 @@
   getContentTdByLine(
     lineNumber: LineNumber,
     side?: Side,
-    root: HTMLElement = this._outputEl
+    root: Element = this._outputEl
   ): Element | null {
     const sideSelector: string = side ? `.${side}` : '';
     return root.querySelector(
@@ -221,17 +221,16 @@
    * @param start The first line number
    * @param end The last line number
    * @param side The side of the range. Either 'left' or 'right'.
-   * @param out_lines The output list of line objects. Use
-   *     null if not desired.
-   * @param out_elements The output list of line elements.
-   *     Use null if not desired.
+   * @param out_lines The output list of line objects. Use null if not desired.
+   * @param out_elements The output list of line elements. Use null if not
+   *        desired.
    */
   findLinesByRange(
     start: LineNumber,
     end: LineNumber,
     side: Side,
-    out_lines: GrDiffLine[],
-    out_elements: HTMLElement[]
+    out_lines: GrDiffLine[] | null,
+    out_elements: HTMLElement[] | null
   ) {
     const groups = this.getGroupsByLineRange(start, end, side);
     for (const group of groups) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.ts b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.ts
index 03dd931..9788c8e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.ts
@@ -28,6 +28,8 @@
 import {Side} from '../../../constants/constants';
 import {CommentRange} from '../../../types/common';
 import {GrSelectionActionBox} from '../gr-selection-action-box/gr-selection-action-box';
+import {GrDiffBuilderElement} from '../gr-diff-builder/gr-diff-builder-element';
+import {FILE} from '../gr-diff/gr-diff-line';
 
 interface SidedRange {
   side: Side;
@@ -51,14 +53,6 @@
   rootId: string;
 }
 
-// TODO(TS): Replace by GrDiffBuilderElement once that is converted.
-interface DiffBuilderElement extends HTMLElement {
-  getLineElByChild(node: Node): HTMLElement;
-  getSideByLineEl(lineEl: HTMLElement): Side;
-  getLineNumberByChild(lineEl: HTMLElement): number;
-  getContentTdByLineEl(lineEl: HTMLElement): HTMLElement;
-}
-
 @customElement('gr-diff-highlight')
 export class GrDiffHighlight extends GestureEventListeners(
   LegacyElementMixin(PolymerElement)
@@ -74,7 +68,7 @@
   loggedIn?: boolean;
 
   @property({type: Object})
-  _cachedDiffBuilder?: DiffBuilderElement;
+  _cachedDiffBuilder?: GrDiffBuilderElement;
 
   @property({type: Object, notify: true})
   selectedRange?: SidedRange;
@@ -97,7 +91,7 @@
     if (!this._cachedDiffBuilder) {
       this._cachedDiffBuilder = this.querySelector(
         'gr-diff-builder'
-      ) as DiffBuilderElement;
+      ) as GrDiffBuilderElement;
     }
     return this._cachedDiffBuilder;
   }
@@ -336,22 +330,15 @@
     offset: number
   ): NormalizedPosition | null {
     let column;
-    if (!node || !this.contains(node)) {
-      return null;
-    }
+    if (!node || !this.contains(node)) return null;
     const lineEl = this.diffBuilder.getLineElByChild(node);
-    if (!lineEl) {
-      return null;
-    }
+    if (!lineEl) return null;
     const side = this.diffBuilder.getSideByLineEl(lineEl);
-    if (!side) {
-      return null;
-    }
+    if (!side) return null;
     const line = this.diffBuilder.getLineNumberByChild(lineEl);
-    if (!line) {
-      return null;
-    }
+    if (!line || line === FILE) return null;
     const contentTd = this.diffBuilder.getContentTdByLineEl(lineEl);
+    if (!contentTd) return null;
     const contentText = contentTd.querySelector('.contentText');
     if (!contentTd.contains(node)) {
       node = contentText;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.ts b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.ts
index 74164a8..0ac39f0 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.ts
@@ -48,7 +48,7 @@
   keyLocation: boolean;
 }
 
-interface KeyLocations {
+export interface KeyLocations {
   left: {[key: string]: boolean};
   right: {[key: string]: boolean};
 }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.ts b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.ts
index e040856..fab2b59 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.ts
@@ -29,15 +29,7 @@
 import {customElement, property, observe} from '@polymer/decorators';
 import {DiffInfo} from '../../../types/common';
 import {Side} from '../../../constants/constants';
-
-// TODO(TS): Replace by GrDiffBuilderElement once that is converted.
-interface DiffBuilderElement extends HTMLElement {
-  diffElement: HTMLTableElement;
-  getLineElByChild(node: Node): HTMLElement;
-  getSideByLineEl(lineEl: HTMLElement): Side;
-  getLineNumberByChild(lineEl: HTMLElement): number;
-  getContentTdByLineEl(lineEl: HTMLElement): HTMLElement;
-}
+import {GrDiffBuilderElement} from '../gr-diff-builder/gr-diff-builder-element';
 
 /**
  * Possible CSS classes indicating the state of selection. Dynamically added/
@@ -71,7 +63,7 @@
   diff?: DiffInfo;
 
   @property({type: Object})
-  _cachedDiffBuilder?: DiffBuilderElement;
+  _cachedDiffBuilder?: GrDiffBuilderElement;
 
   @property({type: Object})
   _linesCache: LinesCache = {left: null, right: null};
@@ -93,7 +85,7 @@
     if (!this._cachedDiffBuilder) {
       this._cachedDiffBuilder = this.querySelector(
         'gr-diff-builder'
-      ) as DiffBuilderElement;
+      ) as GrDiffBuilderElement;
     }
     return this._cachedDiffBuilder;
   }
@@ -132,7 +124,7 @@
 
     if (blameSelected) {
       targetClasses.push(SelectionClass.BLAME);
-    } else {
+    } else if (lineEl) {
       const commentSelected = this._elementDescendedFromClass(
         target,
         'gr-comment'
@@ -237,6 +229,7 @@
     }
     const range = normalize(sel.getRangeAt(0));
     const startLineEl = this.diffBuilder.getLineElByChild(range.startContainer);
+    if (!startLineEl) return;
     const endLineEl = this.diffBuilder.getLineElByChild(range.endContainer);
     // Happens when triple click in side-by-side mode with other side empty.
     const endsAtOtherEmptySide =
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.ts
index f983428..2d80213 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.ts
@@ -15,6 +15,7 @@
  * limitations under the License.
  */
 
+export const FILE = 'FILE';
 export type LineNumber = number | 'FILE';
 
 export enum GrDiffLineType {
@@ -24,8 +25,6 @@
   REMOVE = 'remove',
 }
 
-export const FILE = 'FILE';
-
 export class GrDiffLine {
   constructor(
     readonly type: GrDiffLineType,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-utils.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-utils.ts
index dbdab05..0aa42c3 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-utils.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-utils.ts
@@ -16,6 +16,7 @@
  */
 
 import {CommentRange} from '../../../types/common';
+import {FILE, LineNumber} from './gr-diff-line';
 
 export enum DiffSide {
   LEFT = 'left',
@@ -41,3 +42,12 @@
     a.end_character === b.end_character
   );
 }
+
+export function getLineNumber(lineEl?: Element | null): LineNumber | null {
+  if (!lineEl) return null;
+  const lineNumberStr = lineEl.getAttribute('data-value');
+  if (!lineNumberStr) return null;
+  if (lineNumberStr === FILE) return FILE;
+  const lineNumber = Number(lineNumberStr);
+  return Number.isInteger(lineNumber) ? lineNumber : null;
+}
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts
index 36a229f..f001a89 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts
@@ -80,7 +80,7 @@
 const HOVER_HIGHLIGHT = 'style-scope gr-diff rangeHighlight';
 
 @customElement('gr-ranged-comment-layer')
-class GrRangedCommentLayer
+export class GrRangedCommentLayer
   extends GestureEventListeners(LegacyElementMixin(PolymerElement))
   implements DiffLayer {
   static get template() {
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.ts b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.ts
index d731ef0..41964d5 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.ts
@@ -35,14 +35,11 @@
   @property({
     type: Object,
     notify: true,
-    observer: GrEndpointParam.prototype._valueChanged,
+    observer: '_valueChanged',
   })
-  value: Record<string, unknown> | undefined = undefined;
+  value?: unknown;
 
-  private _valueChanged(
-    newValue: Record<string, unknown>,
-    _oldValue: Record<string, unknown>
-  ) {
+  _valueChanged(value: unknown) {
     /* In polymer 2 the following change was made:
     "Property change notifications (property-changed events) aren't fired when
     the value changes as a result of a binding from the host"
@@ -51,9 +48,6 @@
     In some cases this fire the event twice, but our code is
     ready for it.
     */
-    const detail = {
-      value: newValue,
-    };
-    this.dispatchEvent(new CustomEvent('value-changed', {detail}));
+    this.dispatchEvent(new CustomEvent('value-changed', {detail: {value}}));
   }
 }
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index 272da74..646cb43 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -1196,10 +1196,10 @@
 export interface ImageInfo {
   body: string;
   type: string;
-  _name: string;
-  _expectedType: string;
-  _width: number;
-  _height: number;
+  _name?: string;
+  _expectedType?: string;
+  _width?: number;
+  _height?: number;
 }
 
 /**
diff --git a/polygerrit-ui/app/types/types.ts b/polygerrit-ui/app/types/types.ts
index 69a550b..54411f8 100644
--- a/polygerrit-ui/app/types/types.ts
+++ b/polygerrit-ui/app/types/types.ts
@@ -14,6 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {Side} from '../constants/constants';
+
+export interface CoverageRange {
+  type: CoverageType;
+  side: Side;
+  code_range: {end_line: number; start_line: number};
+}
 
 export enum CoverageType {
   /**