TypeScript · Course navigation

TypeScript: a grayscale image class with threshold and flip

Wrap a flat pixel buffer in a class that checks bounds and returns new images from its operations.

On this page
Before you start

TypeScript classes and typed arrays.

  • Wrap a flat pixel buffer in a class that checks bounds and returns new images from its operations.
  • Predict the sample output and explain the state changes.
  • Complete the practice task and check the boundary cases.

The concept

A grayscale image is a grid of brightness values. Storing it in one flat Uint8Array of width × height bytes is compact and fast, and the pixel at column x and row y lives at index y × width + x. A class hides this index arithmetic and checks that coordinates are inside the image.

Thresholding turns each pixel into 255 or 0 depending on whether it is brighter than a cutoff, giving a binary image. A horizontal flip mirrors each row. Both return a new image and leave the original unchanged, which makes the operations easy to reason about and to test. The toText method draws bright pixels as # and dark ones as a dot.

Code example

TypeScript
class GrayImage {
  readonly width: number;
  readonly height: number;
  private data: Uint8Array;

  constructor(width: number, height: number, data?: Uint8Array) {
    this.width = width;
    this.height = height;
    this.data = data ?? new Uint8Array(width * height);
  }

  static fromRows(rows: number[][]): GrayImage {
    const image = new GrayImage(rows[0].length, rows.length);
    rows.forEach((row, y) => row.forEach((value, x) => image.set(x, y, value)));
    return image;
  }

  get(x: number, y: number): number {
    if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
      throw new RangeError("pixel (" + x + "," + y + ") is outside the image");
    }
    return this.data[y * this.width + x];
  }

  set(x: number, y: number, value: number): void {
    this.get(x, y);
    this.data[y * this.width + x] = value;
  }

  threshold(cutoff: number): GrayImage {
    return new GrayImage(this.width, this.height, this.data.map((v) => (v > cutoff ? 255 : 0)));
  }

  flipHorizontal(): GrayImage {
    const out = new GrayImage(this.width, this.height);
    for (let y = 0; y < this.height; y++) {
      for (let x = 0; x < this.width; x++) {
        out.set(this.width - 1 - x, y, this.get(x, y));
      }
    }
    return out;
  }

  toText(): string {
    const lines: string[] = [];
    for (let y = 0; y < this.height; y++) {
      let line = "";
      for (let x = 0; x < this.width; x++) {
        line += this.get(x, y) > 127 ? "#" : ".";
      }
      lines.push(line);
    }
    return lines.join("\n");
  }
}

const image = GrayImage.fromRows([
  [10, 200, 30, 220],
  [240, 20, 250, 40],
  [60, 180, 70, 190],
]);
console.log(image.threshold(128).toText());
console.log("--");
console.log(image.threshold(128).flipHorizontal().toText());
try {
  image.get(4, 0);
} catch (error) {
  console.log((error as Error).message);
}
Output
.#.#
#.#.
.#.#
--
#.#.
.#.#
#.#.
pixel (4,0) is outside the image

Follow the code

Open the walkthrough to inspect the example one step at a time.

Show guided walkthrough
Guided walkthroughReady
1class GrayImage {
2 readonly width: number;
3 readonly height: number;
4 private data: Uint8Array;
5
6 constructor(width: number, height: number, data?: Uint8Array) {
7 this.width = width;
8 this.height = height;
9 this.data = data ?? new Uint8Array(width * height);
10 }
11
12 static fromRows(rows: number[][]): GrayImage {
13 const image = new GrayImage(rows[0].length, rows.length);
14 rows.forEach((row, y) => row.forEach((value, x) => image.set(x, y, value)));
15 return image;
16 }
17
18 get(x: number, y: number): number {
19 if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
20 throw new RangeError("pixel (" + x + "," + y + ") is outside the image");
21 }
22 return this.data[y * this.width + x];
23 }
24
25 set(x: number, y: number, value: number): void {
26 this.get(x, y);
27 this.data[y * this.width + x] = value;
28 }
29
30 threshold(cutoff: number): GrayImage {
31 return new GrayImage(this.width, this.height, this.data.map((v) => (v > cutoff ? 255 : 0)));
32 }
33
34 flipHorizontal(): GrayImage {
35 const out = new GrayImage(this.width, this.height);
36 for (let y = 0; y < this.height; y++) {
37 for (let x = 0; x < this.width; x++) {
38 out.set(this.width - 1 - x, y, this.get(x, y));
39 }
40 }
41 return out;
42 }
43
44 toText(): string {
45 const lines: string[] = [];
46 for (let y = 0; y < this.height; y++) {
47 let line = "";
48 for (let x = 0; x < this.width; x++) {
49 line += this.get(x, y) > 127 ? "#" : ".";
50 }
51 lines.push(line);
52 }
53 return lines.join("\n");
54 }
55}
56
57const image = GrayImage.fromRows([
58 [10, 200, 30, 220],
59 [240, 20, 250, 40],
60 [60, 180, 70, 190],
61]);
62console.log(image.threshold(128).toText());
63console.log("--");
64console.log(image.threshold(128).flipHorizontal().toText());
65try {
66 image.get(4, 0);
67} catch (error) {
68 console.log((error as Error).message);
69}

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
Row-major layout: the pixel at (x, y) is at index y × width + x.—
—
Out-of-range coordinates are rejected instead of reading the wrong row.—
—
Each pixel above the cutoff becomes 255; the others become 0. The original image is untouched.—
—
Bright pixels are drawn as #.—
.#.#
#.#.
.#.#
Each row is mirrored left to right.—
.#.#
#.#.
.#.#
--
#.#.
.#.#
#.#.
x = 4 is outside a 4-pixel-wide image (valid x is 0 to 3).—
.#.#
#.#.
.#.#
--
#.#.
.#.#
#.#.
pixel (4,0) is outside the image

Behind the scenes

How TypeScript processes this code

TypeScript checks types and emits JavaScript. Type annotations are removed before execution.

Cost and common mistakes

get and set are O(1); threshold, flip, and toText are O(width × height).

  • Without the bounds check, get(4, 0) would silently return the first pixel of the next row.
  • Uint8Array stores only 0 to 255. Larger values wrap around, so clamp them before storing.
  • Methods that return new images use extra memory. For large images, in-place operations or views are common.

Try it yourself

What does flipHorizontal().flipHorizontal() give?

Reveal answer and reasoning

The original image, because mirroring twice restores every row.

Further reading