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
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
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);
}.#.# #.#. .#.# -- #.#. .#.# #.#. 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
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);}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
| Step / line | What happens | State after step | Output 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.