DEV Community

Cover image for Low-Level Serialization: Writing an Array-Based Hex to Base64 Encoder in TypeScript
kandz
kandz

Posted on

Low-Level Serialization: Writing an Array-Based Hex to Base64 Encoder in TypeScript

When working with low-level data streams, cryptographic keys, file attachments, or binary payloads in web applications, we frequently need to serialize raw bytes into printable text.

The two most prominent serialization systems are Hexadecimal (Base-16) and Base-64.

While standard JavaScript runtimes provide basic helpers like btoa() and atob() for Base64 string encoding, these built-in methods are notoriously buggy when dealing with raw binary byte arrays or non-ASCII characters, and they do not support hexadecimal inputs out-of-the-box.

Here is how these two base systems map mathematically, the bit-shifting logic required to translate them, and how to write a high-performance, bidirectional Hex-to-Base64 converter in vanilla TypeScript with zero external dependencies.


The Mathematical Divide: Base-16 vs. Base-64

Both systems represent raw binary data, but they use different density scales:

  • Hexadecimal (Base-16): Represents data using 16 symbols (0-9 and a-f). Each character represents exactly 4 bits (a nibble). This means it takes exactly 2 characters to represent 1 byte (8 bits) of data (e.g., 4b represents the decimal value 75).
  • Base-64 (Base-64): Represents data using 64 printable characters (A-Z, a-z, 0-9, +, and /). Each character represents exactly 6 bits.

Because of this, 3 bytes of binary data (24 bits) fit into exactly 4 Base64 characters (24 bits). In contrast, representing those same 3 bytes in Hexadecimal requires 6 characters. Base64 is significantly more compact, saving approximately 33% in file size over Hex for network transmission.


Writing the Encoder in TypeScript

To convert a Hexadecimal string to Base64, we cannot do a simple string replace. We must first parse the Hex characters into raw 8-bit bytes (a Uint8Array), and then encode those bytes into 6-bit Base64 index chunks.

Here is the complete, self-contained TypeScript implementation:

class HexBase64Converter {
  private static readonly CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';

  // 1. Convert raw bytes (Uint8Array) to a Base64 string
  static bytesToBase64(bytes: Uint8Array): string {
    let result = '';
    const l = bytes.length;

    for (let i = 2; i < l; i += 3) {
      result += this.CHARS[bytes[i - 2] >> 2];
      result += this.CHARS[((bytes[i - 2] & 3) << 4) | (bytes[i - 1] >> 4)];
      result += this.CHARS[((bytes[i - 1] & 15) << 2) | (bytes[i] >> 6)];
      result += this.CHARS[bytes[i] & 63];
    }

    // Handle remaining bytes (Padding)
    if (l % 3 === 1) {
      result += this.CHARS[bytes[l - 1] >> 2];
      result += this.CHARS[(bytes[l - 1] & 3) << 4];
      result += '==';
    } else if (l % 3 === 2) {
      result += this.CHARS[bytes[l - 2] >> 2];
      result += this.CHARS[((bytes[l - 2] & 3) << 4) | (bytes[l - 1] >> 4)];
      result += this.CHARS[(bytes[l - 1] & 15) << 2];
      result += '=';
    }
    return result;
  }

  // 2. Convert Hexadecimal string to raw bytes
  static hexToBytes(hexStr: string): Uint8Array {
    const cleanHex = hexStr.replace(/[^0-9a-fA-F]/g, '');
    if (cleanHex.length % 2 !== 0) {
      throw new Error('Invalid Hexadecimal sequence (odd length)');
    }

    const bytes = new Uint8Array(cleanHex.length / 2);
    for (let i = 0; i < cleanHex.length; i += 2) {
      bytes[i / 2] = parseInt(cleanHex.substring(i, i + 2), 16);
    }
    return bytes;
  }

  // 3. Facade method: Hex to Base64
  static hexToBase64(hexStr: string): string {
    const bytes = this.hexToBytes(hexStr);
    return this.bytesToBase64(bytes);
  }
}

// Example usage:
const hex = '4b616e645a20546f6f6c73'; // Hex for "KandZ Tools"
console.log(HexBase64Converter.hexToBase64(hex)); // "S2FuZFogVG9vbHM="
Enter fullscreen mode Exit fullscreen mode

Why This Method Is Bulletproof:

  1. Zero-Dependencies: Does not rely on Node's Buffer or browser-only btoa() / atob() window wrappers. It is 100% Server-Side Rendering (SSR) safe and compiles under any JavaScript runtime.
  2. Explicit Bit-Shifting: Uses direct bitwise shifts (>> and &) to extract the exact 6-bit chunks from the 8-bit byte array, ensuring high-speed processing for large binary payloads.
  3. Strict Padding Compliance: Handles standard mathematical padding (= and ==) for asymmetrical byte boundaries.

Secure Local Data Handling

When testing API integrations, compiling security keys, or analyzing database hashes, developers frequently need to translate data between Hex and Base64. Uploading these sensitive raw strings to server-side logging converters exposes your system credentials and cryptographic keys to remote databases.

To address this, we built a free, 100% Client-Side Hex to Base64 & Base64 to Hex Converter at KandZ Tools.

Our utility executes all bitwise conversions exclusively inside your browser's local memory (RAM). Your keys, tokens, and structural binary streams are never transmitted over the network, keeping your development workflows completely confidential.

Convert your data packages securely: https://tools.kandz.me/hex-base64-converter

Top comments (0)