Convert gr-diff helpers to TypeScript

Change-Id: If09a43e8d890f0ea51ca997b3281575c2e61d084
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.ts
index 34cced1..b6826f3 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.ts
@@ -14,70 +14,33 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {GrDiffLine} from './gr-diff-line.js';
+import {BLANK_LINE, GrDiffLine, GrDiffLineType} from './gr-diff-line';
 
-/**
- * A chunk of the diff that should be rendered together.
- *
- * @constructor
- * @param {!GrDiffGroup.Type} type
- * @param {!Array<!GrDiffLine>=} opt_lines
- */
-export function GrDiffGroup(type, opt_lines) {
-  /** @type {!GrDiffGroup.Type} */
-  this.type = type;
-
-  /** @type {boolean} */
-  this.dueToRebase = false;
-
-  /**
-   * True means all changes in this line are whitespace changes that should
-   * not be highlighted as changed as per the user settings.
-   *
-   * @type{boolean}
-   */
-  this.ignoredWhitespaceOnly = false;
-
-  /**
-   * True means it should not be collapsed (because it was in the URL, or
-   * there is a comment on that line)
-   */
-  this.keyLocation = false;
-
-  /** @type {?HTMLElement} */
-  this.element = null;
-
-  /** @type {!Array<!GrDiffLine>} */
-  this.lines = [];
-  /** @type {!Array<!GrDiffLine>} */
-  this.adds = [];
-  /** @type {!Array<!GrDiffLine>} */
-  this.removes = [];
-  /** @type {?Array<Object>} ?Array<!GrDiffGroup> */
-  this.contextGroups = null;
-
-  /** Both start and end line are inclusive. */
-  this.lineRange = {
-    left: {start: null, end: null},
-    right: {start: null, end: null},
-  };
-
-  if (opt_lines) {
-    opt_lines.forEach(this.addLine, this);
-  }
-}
-
-/** @enum {string} */
-GrDiffGroup.Type = {
+export enum GrDiffGroupType {
   /** Unchanged context. */
-  BOTH: 'both',
+  BOTH = 'both',
 
   /** A widget used to show more context. */
-  CONTEXT_CONTROL: 'contextControl',
+  CONTEXT_CONTROL = 'contextControl',
 
   /** Added, removed or modified chunk. */
-  DELTA: 'delta',
-};
+  DELTA = 'delta',
+}
+
+export interface GrDiffLinePair {
+  left: GrDiffLine;
+  right: GrDiffLine;
+}
+
+interface Range {
+  start: number | null;
+  end: number | null;
+}
+
+interface GrDiffGroupRange {
+  left: Range;
+  right: Range;
+}
 
 /**
  * Hides lines in the given range behind a context control group.
@@ -91,36 +54,37 @@
  * If the hidden range is 1 line or less, nothing is hidden and no context
  * control group is created.
  *
- * @param {!Array<!GrDiffGroup>} groups Common groups, ordered by their line
- *     ranges.
- * @param {number} hiddenStart The first element to be hidden, as a
+ * @param groups Common groups, ordered by their line ranges.
+ * @param hiddenStart The first element to be hidden, as a
  *     non-negative line number offset relative to the first group's start
  *     line, left and right respectively.
- * @param {number} hiddenEnd The first visible element after the hidden range,
+ * @param hiddenEnd The first visible element after the hidden range,
  *     as a non-negative line number offset relative to the first group's
  *     start line, left and right respectively.
- * @return {!Array<!GrDiffGroup>}
  */
-GrDiffGroup.hideInContextControl = function(groups, hiddenStart, hiddenEnd) {
+export function hideInContextControl(
+  groups: GrDiffGroup[],
+  hiddenStart: number,
+  hiddenEnd: number
+): GrDiffGroup[] {
   if (groups.length === 0) return [];
   // Clamp hiddenStart and hiddenEnd - inspired by e.g. substring
   hiddenStart = Math.max(hiddenStart, 0);
   hiddenEnd = Math.max(hiddenEnd, hiddenStart);
 
-  let before = [];
+  let before: GrDiffGroup[] = [];
   let hidden = groups;
-  let after = [];
+  let after: GrDiffGroup[] = [];
 
   const numHidden = hiddenEnd - hiddenStart;
 
   // Only collapse if there is more than 1 line to be hidden.
   if (numHidden > 1) {
     if (hiddenStart) {
-      [before, hidden] = GrDiffGroup._splitCommonGroups(hidden, hiddenStart);
+      [before, hidden] = _splitCommonGroups(hidden, hiddenStart);
     }
     if (hiddenEnd) {
-      [hidden, after] = GrDiffGroup._splitCommonGroups(
-          hidden, hiddenEnd - hiddenStart);
+      [hidden, after] = _splitCommonGroups(hidden, hiddenEnd - hiddenStart);
     }
   } else {
     [hidden, after] = [[], hidden];
@@ -128,14 +92,13 @@
 
   const result = [...before];
   if (hidden.length) {
-    const ctxGroup = new GrDiffGroup(
-        GrDiffGroup.Type.CONTEXT_CONTROL, []);
+    const ctxGroup = new GrDiffGroup(GrDiffGroupType.CONTEXT_CONTROL, []);
     ctxGroup.contextGroups = hidden;
     result.push(ctxGroup);
   }
   result.push(...after);
   return result;
-};
+}
 
 /**
  * Splits a list of common groups into two lists of groups.
@@ -145,27 +108,34 @@
  * with some lines before and some lines after the split will be split into
  * two groups, which will be put into the first and second list.
  *
- * @param {!Array<!GrDiffGroup>} groups
- * @param {number} split A line number offset relative to the first group's
+ * @param groups
+ * @param split A line number offset relative to the first group's
  *     start line at which the groups should be split.
- * @return {!Array<!Array<!GrDiffGroup>>} The outer array has 2 elements, the
+ * @return The outer array has 2 elements, the
  *   list of groups before and the list of groups after the split.
  */
-GrDiffGroup._splitCommonGroups = function(groups, split) {
+function _splitCommonGroups(
+  groups: GrDiffGroup[],
+  split: number
+): GrDiffGroup[][] {
   if (groups.length === 0) return [[], []];
-  const leftSplit = groups[0].lineRange.left.start + split;
-  const rightSplit = groups[0].lineRange.right.start + split;
+  const leftSplit = (groups[0].lineRange.left.start || 0) + split;
+  const rightSplit = (groups[0].lineRange.right.start || 0) + split;
 
   const beforeGroups = [];
   const afterGroups = [];
   for (const group of groups) {
-    if (group.lineRange.left.end < leftSplit ||
-        group.lineRange.right.end < rightSplit) {
+    if (
+      (group.lineRange.left.end || 0) < leftSplit ||
+      (group.lineRange.right.end || 0) < rightSplit
+    ) {
       beforeGroups.push(group);
       continue;
     }
-    if (leftSplit <= group.lineRange.left.start ||
-        rightSplit <= group.lineRange.right.start) {
+    if (
+      leftSplit <= (group.lineRange.left.start || 0) ||
+      rightSplit <= (group.lineRange.right.start || 0)
+    ) {
       afterGroups.push(group);
       continue;
     }
@@ -173,8 +143,10 @@
     const before = [];
     const after = [];
     for (const line of group.lines) {
-      if ((line.beforeNumber && line.beforeNumber < leftSplit) ||
-          (line.afterNumber && line.afterNumber < rightSplit)) {
+      if (
+        (line.beforeNumber && line.beforeNumber < leftSplit) ||
+        (line.afterNumber && line.afterNumber < rightSplit)
+      ) {
         before.push(line);
       } else {
         after.push(line);
@@ -182,102 +154,162 @@
     }
 
     if (before.length) {
-      beforeGroups.push(before.length === group.lines.length ?
-        group : group.cloneWithLines(before));
+      beforeGroups.push(
+        before.length === group.lines.length
+          ? group
+          : group.cloneWithLines(before)
+      );
     }
     if (after.length) {
-      afterGroups.push(after.length === group.lines.length ?
-        group : group.cloneWithLines(after));
+      afterGroups.push(
+        after.length === group.lines.length
+          ? group
+          : group.cloneWithLines(after)
+      );
     }
   }
   return [beforeGroups, afterGroups];
-};
+}
 
 /**
- * Creates a new group with the same properties but different lines.
+ * A chunk of the diff that should be rendered together.
  *
- * The element property is not copied, because the original element is still a
- * rendering of the old lines, so that would not make sense.
- *
- * @param {!Array<!GrDiffLine>} lines
- * @return {!GrDiffGroup}
+ * @constructor
+ * @param {!GrDiffGroupType} type
+ * @param {!Array<!GrDiffLine>=} opt_lines
  */
-GrDiffGroup.prototype.cloneWithLines = function(lines) {
-  const group = new GrDiffGroup(this.type, lines);
-  group.dueToRebase = this.dueToRebase;
-  group.ignoredWhitespaceOnly = this.ignoredWhitespaceOnly;
-  return group;
-};
-
-/** @param {!GrDiffLine} line */
-GrDiffGroup.prototype.addLine = function(line) {
-  this.lines.push(line);
-
-  const notDelta = (this.type === GrDiffGroup.Type.BOTH ||
-      this.type === GrDiffGroup.Type.CONTEXT_CONTROL);
-  if (notDelta && (line.type === GrDiffLine.Type.ADD ||
-      line.type === GrDiffLine.Type.REMOVE)) {
-    throw Error('Cannot add delta line to a non-delta group.');
+export class GrDiffGroup {
+  constructor(readonly type: GrDiffGroupType, lines: GrDiffLine[] = []) {
+    lines.forEach((line: GrDiffLine) => this.addLine(line));
   }
 
-  if (line.type === GrDiffLine.Type.ADD) {
-    this.adds.push(line);
-  } else if (line.type === GrDiffLine.Type.REMOVE) {
-    this.removes.push(line);
-  }
-  this._updateRange(line);
-};
+  dueToRebase = false;
 
-/** @return {!Array<{left: GrDiffLine, right: GrDiffLine}>} */
-GrDiffGroup.prototype.getSideBySidePairs = function() {
-  if (this.type === GrDiffGroup.Type.BOTH ||
-      this.type === GrDiffGroup.Type.CONTEXT_CONTROL) {
-    return this.lines.map(line => {
-      return {
-        left: line,
-        right: line,
-      };
-    });
+  /**
+   * True means all changes in this line are whitespace changes that should
+   * not be highlighted as changed as per the user settings.
+   */
+  ignoredWhitespaceOnly = false;
+
+  /**
+   * True means it should not be collapsed (because it was in the URL, or
+   * there is a comment on that line)
+   */
+  keyLocation = false;
+
+  element: HTMLElement | null = null;
+
+  lines: GrDiffLine[] = [];
+
+  adds: GrDiffLine[] = [];
+
+  removes: GrDiffLine[] = [];
+
+  contextGroups: GrDiffGroup[] = [];
+
+  /** Both start and end line are inclusive. */
+  lineRange: GrDiffGroupRange = {
+    left: {start: null, end: null},
+    right: {start: null, end: null},
+  };
+
+  /**
+   * Creates a new group with the same properties but different lines.
+   *
+   * The element property is not copied, because the original element is still a
+   * rendering of the old lines, so that would not make sense.
+   */
+  cloneWithLines(lines: GrDiffLine[]): GrDiffGroup {
+    const group = new GrDiffGroup(this.type, lines);
+    group.dueToRebase = this.dueToRebase;
+    group.ignoredWhitespaceOnly = this.ignoredWhitespaceOnly;
+    return group;
   }
 
-  const pairs = [];
-  let i = 0;
-  let j = 0;
-  while (i < this.removes.length || j < this.adds.length) {
-    pairs.push({
-      left: this.removes[i] || GrDiffLine.BLANK_LINE,
-      right: this.adds[j] || GrDiffLine.BLANK_LINE,
-    });
-    i++;
-    j++;
-  }
-  return pairs;
-};
+  addLine(line: GrDiffLine) {
+    this.lines.push(line);
 
-GrDiffGroup.prototype._updateRange = function(line) {
-  if (line.beforeNumber === 'FILE' || line.afterNumber === 'FILE') { return; }
-
-  if (line.type === GrDiffLine.Type.ADD ||
-      line.type === GrDiffLine.Type.BOTH) {
-    if (this.lineRange.right.start === null ||
-        line.afterNumber < this.lineRange.right.start) {
-      this.lineRange.right.start = line.afterNumber;
+    const notDelta =
+      this.type === GrDiffGroupType.BOTH ||
+      this.type === GrDiffGroupType.CONTEXT_CONTROL;
+    if (
+      notDelta &&
+      (line.type === GrDiffLineType.ADD || line.type === GrDiffLineType.REMOVE)
+    ) {
+      throw Error('Cannot add delta line to a non-delta group.');
     }
-    if (this.lineRange.right.end === null ||
-        line.afterNumber > this.lineRange.right.end) {
-      this.lineRange.right.end = line.afterNumber;
+
+    if (line.type === GrDiffLineType.ADD) {
+      this.adds.push(line);
+    } else if (line.type === GrDiffLineType.REMOVE) {
+      this.removes.push(line);
+    }
+    this._updateRange(line);
+  }
+
+  getSideBySidePairs(): GrDiffLinePair[] {
+    if (
+      this.type === GrDiffGroupType.BOTH ||
+      this.type === GrDiffGroupType.CONTEXT_CONTROL
+    ) {
+      return this.lines.map(line => {
+        return {
+          left: line,
+          right: line,
+        };
+      });
+    }
+
+    const pairs: GrDiffLinePair[] = [];
+    let i = 0;
+    let j = 0;
+    while (i < this.removes.length || j < this.adds.length) {
+      pairs.push({
+        left: this.removes[i] || BLANK_LINE,
+        right: this.adds[j] || BLANK_LINE,
+      });
+      i++;
+      j++;
+    }
+    return pairs;
+  }
+
+  _updateRange(line: GrDiffLine) {
+    if (line.beforeNumber === 'FILE' || line.afterNumber === 'FILE') {
+      return;
+    }
+
+    if (line.type === GrDiffLineType.ADD || line.type === GrDiffLineType.BOTH) {
+      if (
+        this.lineRange.right.start === null ||
+        line.afterNumber < this.lineRange.right.start
+      ) {
+        this.lineRange.right.start = line.afterNumber;
+      }
+      if (
+        this.lineRange.right.end === null ||
+        line.afterNumber > this.lineRange.right.end
+      ) {
+        this.lineRange.right.end = line.afterNumber;
+      }
+    }
+
+    if (
+      line.type === GrDiffLineType.REMOVE ||
+      line.type === GrDiffLineType.BOTH
+    ) {
+      if (
+        this.lineRange.left.start === null ||
+        line.beforeNumber < this.lineRange.left.start
+      ) {
+        this.lineRange.left.start = line.beforeNumber;
+      }
+      if (
+        this.lineRange.left.end === null ||
+        line.beforeNumber > this.lineRange.left.end
+      ) {
+        this.lineRange.left.end = line.beforeNumber;
+      }
     }
   }
-
-  if (line.type === GrDiffLine.Type.REMOVE ||
-      line.type === GrDiffLine.Type.BOTH) {
-    if (this.lineRange.left.start === null ||
-        line.beforeNumber < this.lineRange.left.start) {
-      this.lineRange.left.start = line.beforeNumber;
-    }
-    if (this.lineRange.left.end === null ||
-        line.beforeNumber > this.lineRange.left.end) {
-      this.lineRange.left.end = line.beforeNumber;
-    }
-  }
-};
+}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.js
index 3f8512b..13fc4d1 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.js
@@ -16,15 +16,15 @@
  */
 
 import '../../../test/common-test-setup-karma.js';
-import {GrDiffLine} from './gr-diff-line.js';
-import {GrDiffGroup} from './gr-diff-group.js';
+import {GrDiffLine, GrDiffLineType, BLANK_LINE} from './gr-diff-line.js';
+import {GrDiffGroup, GrDiffGroupType, hideInContextControl} from './gr-diff-group.js';
 
 suite('gr-diff-group tests', () => {
   test('delta line pairs', () => {
-    let group = new GrDiffGroup(GrDiffGroup.Type.DELTA);
-    const l1 = new GrDiffLine(GrDiffLine.Type.ADD, 0, 128);
-    const l2 = new GrDiffLine(GrDiffLine.Type.ADD, 0, 129);
-    const l3 = new GrDiffLine(GrDiffLine.Type.REMOVE, 64, 0);
+    let group = new GrDiffGroup(GrDiffGroupType.DELTA);
+    const l1 = new GrDiffLine(GrDiffLineType.ADD, 0, 128);
+    const l2 = new GrDiffLine(GrDiffLineType.ADD, 0, 129);
+    const l3 = new GrDiffLine(GrDiffLineType.REMOVE, 64, 0);
     group.addLine(l1);
     group.addLine(l2);
     group.addLine(l3);
@@ -39,10 +39,10 @@
     let pairs = group.getSideBySidePairs();
     assert.deepEqual(pairs, [
       {left: l3, right: l1},
-      {left: GrDiffLine.BLANK_LINE, right: l2},
+      {left: BLANK_LINE, right: l2},
     ]);
 
-    group = new GrDiffGroup(GrDiffGroup.Type.DELTA, [l1, l2, l3]);
+    group = new GrDiffGroup(GrDiffGroupType.DELTA, [l1, l2, l3]);
     assert.deepEqual(group.lines, [l1, l2, l3]);
     assert.deepEqual(group.adds, [l1, l2]);
     assert.deepEqual(group.removes, [l3]);
@@ -50,16 +50,16 @@
     pairs = group.getSideBySidePairs();
     assert.deepEqual(pairs, [
       {left: l3, right: l1},
-      {left: GrDiffLine.BLANK_LINE, right: l2},
+      {left: BLANK_LINE, right: l2},
     ]);
   });
 
   test('group/header line pairs', () => {
-    const l1 = new GrDiffLine(GrDiffLine.Type.BOTH, 64, 128);
-    const l2 = new GrDiffLine(GrDiffLine.Type.BOTH, 65, 129);
-    const l3 = new GrDiffLine(GrDiffLine.Type.BOTH, 66, 130);
+    const l1 = new GrDiffLine(GrDiffLineType.BOTH, 64, 128);
+    const l2 = new GrDiffLine(GrDiffLineType.BOTH, 65, 129);
+    const l3 = new GrDiffLine(GrDiffLineType.BOTH, 66, 130);
 
-    let group = new GrDiffGroup(GrDiffGroup.Type.BOTH, [l1, l2, l3]);
+    let group = new GrDiffGroup(GrDiffGroupType.BOTH, [l1, l2, l3]);
 
     assert.deepEqual(group.lines, [l1, l2, l3]);
     assert.deepEqual(group.adds, []);
@@ -77,7 +77,7 @@
       {left: l3, right: l3},
     ]);
 
-    group = new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL, [l1, l2, l3]);
+    group = new GrDiffGroup(GrDiffGroupType.CONTEXT_CONTROL, [l1, l2, l3]);
     assert.deepEqual(group.lines, [l1, l2, l3]);
     assert.deepEqual(group.adds, []);
     assert.deepEqual(group.removes, []);
@@ -91,16 +91,16 @@
   });
 
   test('adding delta lines to non-delta group', () => {
-    const l1 = new GrDiffLine(GrDiffLine.Type.ADD);
-    const l2 = new GrDiffLine(GrDiffLine.Type.REMOVE);
-    const l3 = new GrDiffLine(GrDiffLine.Type.BOTH);
+    const l1 = new GrDiffLine(GrDiffLineType.ADD);
+    const l2 = new GrDiffLine(GrDiffLineType.REMOVE);
+    const l3 = new GrDiffLine(GrDiffLineType.BOTH);
 
-    let group = new GrDiffGroup(GrDiffGroup.Type.BOTH);
+    let group = new GrDiffGroup(GrDiffGroupType.BOTH);
     assert.throws(group.addLine.bind(group, l1));
     assert.throws(group.addLine.bind(group, l2));
     assert.doesNotThrow(group.addLine.bind(group, l3));
 
-    group = new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL);
+    group = new GrDiffGroup(GrDiffGroupType.CONTEXT_CONTROL);
     assert.throws(group.addLine.bind(group, l1));
     assert.throws(group.addLine.bind(group, l2));
     assert.doesNotThrow(group.addLine.bind(group, l3));
@@ -110,34 +110,34 @@
     let groups;
     setup(() => {
       groups = [
-        new GrDiffGroup(GrDiffGroup.Type.BOTH, [
-          new GrDiffLine(GrDiffLine.Type.BOTH, 5, 7),
-          new GrDiffLine(GrDiffLine.Type.BOTH, 6, 8),
-          new GrDiffLine(GrDiffLine.Type.BOTH, 7, 9),
+        new GrDiffGroup(GrDiffGroupType.BOTH, [
+          new GrDiffLine(GrDiffLineType.BOTH, 5, 7),
+          new GrDiffLine(GrDiffLineType.BOTH, 6, 8),
+          new GrDiffLine(GrDiffLineType.BOTH, 7, 9),
         ]),
-        new GrDiffGroup(GrDiffGroup.Type.DELTA, [
-          new GrDiffLine(GrDiffLine.Type.REMOVE, 8),
-          new GrDiffLine(GrDiffLine.Type.ADD, 0, 10),
-          new GrDiffLine(GrDiffLine.Type.REMOVE, 9),
-          new GrDiffLine(GrDiffLine.Type.ADD, 0, 11),
-          new GrDiffLine(GrDiffLine.Type.REMOVE, 10),
-          new GrDiffLine(GrDiffLine.Type.ADD, 0, 12),
+        new GrDiffGroup(GrDiffGroupType.DELTA, [
+          new GrDiffLine(GrDiffLineType.REMOVE, 8),
+          new GrDiffLine(GrDiffLineType.ADD, 0, 10),
+          new GrDiffLine(GrDiffLineType.REMOVE, 9),
+          new GrDiffLine(GrDiffLineType.ADD, 0, 11),
+          new GrDiffLine(GrDiffLineType.REMOVE, 10),
+          new GrDiffLine(GrDiffLineType.ADD, 0, 12),
         ]),
-        new GrDiffGroup(GrDiffGroup.Type.BOTH, [
-          new GrDiffLine(GrDiffLine.Type.BOTH, 11, 13),
-          new GrDiffLine(GrDiffLine.Type.BOTH, 12, 14),
-          new GrDiffLine(GrDiffLine.Type.BOTH, 13, 15),
+        new GrDiffGroup(GrDiffGroupType.BOTH, [
+          new GrDiffLine(GrDiffLineType.BOTH, 11, 13),
+          new GrDiffLine(GrDiffLineType.BOTH, 12, 14),
+          new GrDiffLine(GrDiffLineType.BOTH, 13, 15),
         ]),
       ];
     });
 
     test('hides hidden groups in context control', () => {
-      const collapsedGroups = GrDiffGroup.hideInContextControl(groups, 3, 6);
+      const collapsedGroups = hideInContextControl(groups, 3, 6);
       assert.equal(collapsedGroups.length, 3);
 
       assert.equal(collapsedGroups[0], groups[0]);
 
-      assert.equal(collapsedGroups[1].type, GrDiffGroup.Type.CONTEXT_CONTROL);
+      assert.equal(collapsedGroups[1].type, GrDiffGroupType.CONTEXT_CONTROL);
       assert.equal(collapsedGroups[1].contextGroups.length, 1);
       assert.equal(collapsedGroups[1].contextGroups[0], groups[1]);
 
@@ -145,20 +145,20 @@
     });
 
     test('splits partially hidden groups', () => {
-      const collapsedGroups = GrDiffGroup.hideInContextControl(groups, 4, 7);
+      const collapsedGroups = hideInContextControl(groups, 4, 7);
       assert.equal(collapsedGroups.length, 4);
       assert.equal(collapsedGroups[0], groups[0]);
 
-      assert.equal(collapsedGroups[1].type, GrDiffGroup.Type.DELTA);
+      assert.equal(collapsedGroups[1].type, GrDiffGroupType.DELTA);
       assert.deepEqual(collapsedGroups[1].adds, [groups[1].adds[0]]);
       assert.deepEqual(collapsedGroups[1].removes, [groups[1].removes[0]]);
 
-      assert.equal(collapsedGroups[2].type, GrDiffGroup.Type.CONTEXT_CONTROL);
+      assert.equal(collapsedGroups[2].type, GrDiffGroupType.CONTEXT_CONTROL);
       assert.equal(collapsedGroups[2].contextGroups.length, 2);
 
       assert.equal(
           collapsedGroups[2].contextGroups[0].type,
-          GrDiffGroup.Type.DELTA);
+          GrDiffGroupType.DELTA);
       assert.deepEqual(
           collapsedGroups[2].contextGroups[0].adds,
           groups[1].adds.slice(1));
@@ -168,23 +168,23 @@
 
       assert.equal(
           collapsedGroups[2].contextGroups[1].type,
-          GrDiffGroup.Type.BOTH);
+          GrDiffGroupType.BOTH);
       assert.deepEqual(
           collapsedGroups[2].contextGroups[1].lines,
           [groups[2].lines[0]]);
 
-      assert.equal(collapsedGroups[3].type, GrDiffGroup.Type.BOTH);
+      assert.equal(collapsedGroups[3].type, GrDiffGroupType.BOTH);
       assert.deepEqual(collapsedGroups[3].lines, groups[2].lines.slice(1));
     });
 
     test('groups unchanged if the hidden range is empty', () => {
       assert.deepEqual(
-          GrDiffGroup.hideInContextControl(groups, 0, 0), groups);
+          hideInContextControl(groups, 0, 0), groups);
     });
 
     test('groups unchanged if there is only 1 line to hide', () => {
       assert.deepEqual(
-          GrDiffGroup.hideInContextControl(groups, 3, 4), groups);
+          hideInContextControl(groups, 3, 4), groups);
     });
   });
 });
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 727cb45..8bfd242 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,37 +15,35 @@
  * limitations under the License.
  */
 
-/**
- * @constructor
- * @param {GrDiffLine.Type} type
- * @param {number|string=} opt_beforeLine
- * @param {number|string=} opt_afterLine
- */
-export function GrDiffLine(type, opt_beforeLine, opt_afterLine) {
-  this.type = type;
+type LineNumber = number | 'FILE';
 
-  /** @type {number|string} */
-  this.beforeNumber = opt_beforeLine || 0;
-
-  /** @type {number|string} */
-  this.afterNumber = opt_afterLine || 0;
-
-  /** @type {boolean} */
-  this.hasIntralineInfo = false;
-
-  /** @type {!Array<GrDiffLine.Highlights>} */
-  this.highlights = [];
-
-  this.text = '';
+export enum GrDiffLineType {
+  ADD = 'add',
+  BOTH = 'both',
+  BLANK = 'blank',
+  REMOVE = 'remove',
 }
 
-/** @enum {string} */
-GrDiffLine.Type = {
-  ADD: 'add',
-  BOTH: 'both',
-  BLANK: 'blank',
-  REMOVE: 'remove',
-};
+export const FILE = 'FILE';
+
+export class GrDiffLine {
+  constructor(
+    readonly type: GrDiffLineType,
+    public beforeNumber: LineNumber = 0,
+    public afterNumber: LineNumber = 0
+  ) {}
+
+  hasIntralineInfo = false;
+
+  readonly highlights: Highlights[] = [];
+
+  text = '';
+
+  // TODO(TS): remove this properties
+  static readonly Type = GrDiffLineType;
+
+  static readonly File = FILE;
+}
 
 /**
  * A line highlight object consists of three fields:
@@ -55,15 +53,11 @@
  * - endIndex: (optional) Index of the character where the highlight should
  *   end. If omitted, the highlight is meant to be a continuation onto the
  *   next line.
- *
- * @typedef {{
- *  contentIndex: number,
- *  startIndex: number,
- *  endIndex: number
- * }}
  */
-GrDiffLine.Highlights;
+export interface Highlights {
+  contentIndex: number;
+  startIndex: number;
+  endIndex?: number;
+}
 
-GrDiffLine.FILE = 'FILE';
-
-GrDiffLine.BLANK_LINE = new GrDiffLine(GrDiffLine.Type.BLANK);
+export const BLANK_LINE = new GrDiffLine(GrDiffLineType.BLANK);
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 7eee071..dbdab05 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
@@ -15,26 +15,29 @@
  * limitations under the License.
  */
 
-/** @enum {string} */
-export const DiffSide = {
-  LEFT: 'left',
-  RIGHT: 'right',
-};
+import {CommentRange} from '../../../types/common';
+
+export enum DiffSide {
+  LEFT = 'left',
+  RIGHT = 'right',
+}
 
 /**
  * Compare two ranges. Either argument may be falsy, but will only return
  * true if both are falsy or if neither are falsy and have the same position
  * values.
- *
- * @param {Range=} a range 1
- * @param {Range=} b range 2
- * @return {boolean}
  */
-export function rangesEqual(a, b) {
-  if (!a && !b) { return true; }
-  if (!a || !b) { return false; }
-  return a.start_line === b.start_line &&
-      a.start_character === b.start_character &&
-      a.end_line === b.end_line &&
-      a.end_character === b.end_character;
+export function rangesEqual(a: CommentRange, b: CommentRange): boolean {
+  if (!a && !b) {
+    return true;
+  }
+  if (!a || !b) {
+    return false;
+  }
+  return (
+    a.start_line === b.start_line &&
+    a.start_character === b.start_character &&
+    a.end_line === b.end_line &&
+    a.end_character === b.end_character
+  );
 }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index 2c5787a..beba588 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -26,7 +26,7 @@
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {htmlTemplate} from './gr-diff_html.js';
-import {GrDiffLine} from './gr-diff-line.js';
+import {FILE} from './gr-diff-line.js';
 import {DiffSide, rangesEqual} from './gr-diff-utils.js';
 import {getHiddenScroll} from '../../../scripts/hiddenscroll.js';
 import {
@@ -395,8 +395,7 @@
 
     for (const threadEl of threadEls) {
       const commentSide = threadEl.getAttribute('comment-side');
-      const lineNum = Number(threadEl.getAttribute('line-num')) ||
-          GrDiffLine.FILE;
+      const lineNum = Number(threadEl.getAttribute('line-num')) || FILE;
       const commentRange = threadEl.range || {};
       keyLocations[commentSide][lineNum] = true;
       // Add start_line as well if exists,
@@ -521,7 +520,7 @@
 
     const value = el.getAttribute('data-value');
     let lineNum;
-    if (value !== GrDiffLine.FILE) {
+    if (value !== FILE) {
       lineNum = parseInt(value, 10);
       if (isNaN(lineNum)) {
         this.dispatchEvent(new CustomEvent('show-alert', {