Skip to content

Instantly share code, notes, and snippets.

@VictorQueiroz
Last active May 6, 2025 10:17
Show Gist options
  • Select an option

  • Save VictorQueiroz/071599802a4e00e591af4e2a41ced5ac to your computer and use it in GitHub Desktop.

Select an option

Save VictorQueiroz/071599802a4e00e591af4e2a41ced5ac to your computer and use it in GitHub Desktop.
Converts a typed array to a different typed array type.
import test, { describe } from "node:test";
import quantizeToFloat32 from "../../quantizeToFloat32";
import castTypedArray from "./castTypedArray";
describe("asTypedArray", async () => {
const { expect } = await import("chai");
test("it should convert Float32Array to Uint8Array without data loss", () => {
const expected = new Array(48)
.fill(0.0)
.map(() => quantizeToFloat32(Math.random()));
const initialInput = new Float32Array(expected);
// Cast from Float32Array to Uint8Array
const output = castTypedArray(initialInput, Uint8Array);
// Cast from Uint8Array to Float32Array
const expectedOutput = castTypedArray(output, Float32Array);
expect(output).not.to.be.deep.equal(initialInput);
expect(initialInput).to.be.deep.equal(expectedOutput);
expect(expectedOutput).to.be.deep.equal(new Float32Array(expected));
expect(initialInput).to.be.deep.equal(new Float32Array(expected));
});
test('it should return the same reference if the input and output types are the same instance', () => {
const input = new Float32Array([0.125, 0.25, 0.5, 0.75, 1]);
const output = castTypedArray(input, Float32Array);
expect(input).to.be.equal(output);
});
describe("Type-checking", () => {
test("it should return an instance of the typed array constructor given as the second argument", async () => {
const testA: Uint8Array = castTypedArray(
new Float32Array([]),
Uint8Array
);
// @ts-expect-error Make sure we are returning the correct typed array
const testB: Float32Array = castTypedArray(testA, Uint8Array);
// @ts-expect-error Make sure we are returning TypedArray<ArrayBuffer>
const testC: Uint8Array<SharedArrayBuffer> = castTypedArray(
testA,
Uint8Array
);
});
test("it should output an instance type of the second argument", () => {
const output = castTypedArray(
new Float32Array([0.125, 0.25, 0.5, 0.75, 1]),
Uint8Array
);
let output2: Float32Array;
// @ts-expect-error Make sure we are returning the correct type in TypeScript
output2 = output;
// Remove `error TS6133: 'output2' is declared but its value is never read.`
output2;
});
});
});
export type TypedArrayConstructor =
| Uint16ArrayConstructor
| Uint8ArrayConstructor
| Uint32ArrayConstructor
| Int16ArrayConstructor
| Int8ArrayConstructor
| Int32ArrayConstructor
| Float64ArrayConstructor
| Float32ArrayConstructor;
export type TypedArray<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike> =
| Uint16Array<TArrayBuffer>
| Uint8Array<TArrayBuffer>
| Uint32Array<TArrayBuffer>
| Int16Array<TArrayBuffer>
| Int8Array<TArrayBuffer>
| Int32Array<TArrayBuffer>
| Float64Array<TArrayBuffer>
| Float32Array<TArrayBuffer>;
// Use a custom class for the error, to avoid `error instanceof Error` to return false when it is an extension of an error.
// Do some research on TypeScript to understand the reason of this approach.
export class ErrorCastTypedArray extends Error {
public constructor(public readonly what: string) {
super(what);
}
}
export interface ITypedArrayConstructor<
T extends TypedArray,
TArrayBuffer extends ArrayBufferLike = ArrayBufferLike
> {
new(buffer: TArrayBuffer, byteOffset?: number, length?: number): T;
new(elements: Iterable<number>): T;
readonly BYTES_PER_ELEMENT: number;
}
/**
* Casts a typed array to another typed array.
*
* Throws an exception if the source buffer is not a multiple of the element size.
*
* @param input The typed array to cast.
* @param TypedArray The typed array constructor to cast to.
* @returns The casted typed array.
*/
export default function castTypedArray<
T extends TypedArray,
U extends TypedArray
>(input: T, TypedArray: ITypedArrayConstructor<U>): U {
// If the `input` is an instance of `TypedArray` constructor,
// just return the same reference back.
if (input instanceof TypedArray) {
return input;
}
/**
* If the typed array is empty, return an empty typed array
*/
if (input.byteLength < 1) {
return new TypedArray([]);
}
/**
* Count of elements in the new typed array. This is needed since
* there are certain typed arrays that are not divisible by the element size,
* such as `Float32Array` and `Float64Array` classes, which has `BYTES_PER_ELEMENT` respectively
* equal to 4 bytes, and 8 bytes.
*/
const elementCount = input.byteLength / TypedArray.BYTES_PER_ELEMENT;
const isValid =
input.byteLength % TypedArray.BYTES_PER_ELEMENT === 0 &&
elementCount % TypedArray.BYTES_PER_ELEMENT === 0;
if (!isValid) {
throw new ErrorCastTypedArray(
[
`The buffer has ${input.byteLength}, but the typed array contains ${TypedArray.BYTES_PER_ELEMENT} bytes per element.`,
`buffer.byteLength / TypedArray.BYTES_PER_ELEMENT = ${input.byteLength / TypedArray.BYTES_PER_ELEMENT}`,
`buffer.byteLength % TypedArray.BYTES_PER_ELEMENT = ${input.byteLength % TypedArray.BYTES_PER_ELEMENT}`
]
.map((s, i) => (i === 0 ? s : `\t${s}`))
.join("\n")
);
}
const bufferClone = new ArrayBuffer(input.byteLength);
const viewClone = new TypedArray(bufferClone, 0, elementCount);
// Transfer the data to the clone
viewClone.set(new TypedArray(input.buffer, input.byteOffset, elementCount));
// TODO: Add support for TypedArray<SharedArrayBuffer>
return viewClone;
}
/**
* Quantize a number to a 32-bit float.
*
* @param originalNumber - The number to quantize. If not provided, a random
* number in [-100, 100) is chosen for demonstration.
* @returns The closest representable 32-bit float value.
*
* This function creates a temporary 32-bit float buffer, assigns the input
* number to it, and reads the value back out. This process performs the
* conversion/quantization to 32-bit precision according to IEEE 754 rounding
* rules. The returned value is still a 64-bit JavaScript 'number' type, but
* its value corresponds precisely to a 32-bit float representation.
*/
export default function quantizeToFloat32(originalNumber: number): number {
// Create a temporary buffer (4 bytes for one float32)
const buffer = new ArrayBuffer(4);
// Create a Float32 view on the buffer
const view = new Float32Array(buffer);
// Determine the number to quantize
// If no number is provided, generate a sample random one for demonstration.
const numToQuantize =
originalNumber !== undefined ? originalNumber : Math.random() * 200 - 100; // Example: random number in [-100, 100)
// Assign the number to the Float32Array view.
// This performs the crucial conversion/quantization to 32-bit precision
// according to IEEE 754 rounding rules.
view[0] = numToQuantize;
// Read the value back from the view.
// This value is now guaranteed to be exactly representable as a 32-bit float.
// It's still stored in a 64-bit JavaScript 'number' type, but its value
// corresponds precisely to a 32-bit float representation.
const quantizedValue = view[0];
return quantizedValue;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment