Skip to content

Instantly share code, notes, and snippets.

@tkrotoff
Created January 30, 2026 14:16
Show Gist options
  • Select an option

  • Save tkrotoff/e7a098557616e1d7186b2b7c0fa3112d to your computer and use it in GitHub Desktop.

Select an option

Save tkrotoff/e7a098557616e1d7186b2b7c0fa3112d to your computer and use it in GitHub Desktop.
Encode/decode UUID to Base64
/* eslint-disable unicorn/prefer-code-point */
import { Base64, Base64url } from './Base64';
import { UUID } from './UUIDType';
// Replace with https://github.com/taskcluster/slugid instead?
// https://github.com/uuidjs/uuid/blob/v8.3.2/src/regex.js
export const uuidRegex =
// eslint-disable-next-line unicorn/better-regex
/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000/i;
function isValid(uuid: UUID) {
// https://github.com/uuidjs/uuid/blob/v8.3.2/src/validate.js
return typeof uuid === 'string' && new RegExp(`^${uuidRegex.source}$`, 'i').test(uuid);
}
function validate(uuid: UUID) {
if (!isValid(uuid)) {
throw new TypeError(`Invalid UUID: '${uuid}'`);
}
}
// [HEX to Base64 converter for JavaScript](https://stackoverflow.com/q/23190056)
// Use https://github.com/beatgammit/base64-js or https://github.com/waitingsong/base64 instead?
const hexRadix = 16;
function hexToBase64(str: string) {
const bin = str.replace(/\w{2}/g, c => String.fromCharCode(Number.parseInt(c, hexRadix)));
return btoa(bin) as Base64;
}
function base64ToHex(str: Base64) {
const bin = atob(str);
const hex = new Array<string>();
for (let i = 0; i < bin.length; i++) {
let tmp = bin.charCodeAt(i).toString(hexRadix);
if (tmp.length === 1) tmp = `0${tmp}`;
hex[hex.length] = tmp;
}
return hex.join('');
}
export function toBase64(uuid: UUID) {
validate(uuid);
const hex = uuid.replace(/-/g, '');
return hexToBase64(hex);
}
// Comparison: https://github.com/ai/nanoid/issues/350
// - UUID, 36 symbols: 3a34ea98-651e-4253-92af-653373a20c51
// - UUID Base64url, 22 symbols: OjTqmGUeQlOSr2Uzc6IMUQ
// - Nano ID, 21 symbols: V1StGXR8_Z5jdHi6B-myT
// - YouTube: 11 symbols: -IvvARlqMW8
export function toBase64url(uuid: UUID) {
validate(uuid);
const base64 = toBase64(uuid);
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') as Base64url;
}
export function fromBase64(base64: Base64) {
const hex = base64ToHex(base64);
const uuid = hex.replace(/(\w{8})(\w{4})(\w{4})(\w{4})(\w{12})/, '$1-$2-$3-$4-$5') as UUID;
validate(uuid);
return uuid;
}
export function fromBase64url(base64url: Base64url) {
const base64 = `${base64url.replace(/-/g, '+').replace(/_/g, '/')}==` as Base64;
const uuid = fromBase64(base64);
validate(uuid);
return uuid;
}
// Must never be used in "real code", only inside unit tests for example
// [Create GUID / UUID in JavaScript?](https://stackoverflow.com/q/105034)
export function dangerouslyGenerate() {
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = Math.trunc(Math.random() * 16);
const v = c === 'x' ? r : (r & 0x3) | 0x8; // eslint-disable-line no-bitwise
return v.toString(16);
}) as UUID;
validate(uuid);
return uuid;
}
import { Opaque } from 'type-fest';
export type Base64 = Opaque<string, 'Base64'>;
// [RFC4648 Base 64 Encoding with URL and Filename Safe Alphabet](https://tools.ietf.org/html/rfc4648#section-5)
// "This encoding may be referred to as "base64url"" => url in lower case
export type Base64url = Opaque<string, 'Base64url'>;
import { Base64url } from './Base64';
import { UUID } from './UUIDType';
import { dangerouslyGenerate, fromBase64, fromBase64url, toBase64, toBase64url } from './UUID';
test('toBase64url() / fromBase64url()', () => {
// https://stackoverflow.com/q/772802
expect(toBase64url('cdaed56d-8712-414d-b346-01905d0026fe' as UUID)).toEqual(
'za7VbYcSQU2zRgGQXQAm_g'
);
expect(fromBase64url('za7VbYcSQU2zRgGQXQAm_g' as Base64url)).toEqual(
'cdaed56d-8712-414d-b346-01905d0026fe'
);
// https://stackoverflow.com/a/15013205
expect(toBase64url('6fcb514b-b878-4c9d-95b7-8dc3a7ce6fd8' as UUID)).toEqual(
'b8tRS7h4TJ2Vt43Dp85v2A'
);
expect(fromBase64url('b8tRS7h4TJ2Vt43Dp85v2A' as Base64url)).toEqual(
'6fcb514b-b878-4c9d-95b7-8dc3a7ce6fd8'
);
// https://stackoverflow.com/a/55474589
expect(toBase64url('eb55c9cc-1fc1-43da-9adb-d9c66bb259ad' as UUID)).toEqual(
'61XJzB_BQ9qa29nGa7JZrQ'
);
expect(fromBase64url('61XJzB_BQ9qa29nGa7JZrQ' as Base64url)).toEqual(
'eb55c9cc-1fc1-43da-9adb-d9c66bb259ad'
);
// https://stackoverflow.com/a/68909992
expect(toBase64url('01234567-89AB-4DEF-A123-456789ABCDEF' as UUID)).toEqual(
'ASNFZ4mrTe-hI0VniavN7w'
);
expect(fromBase64url('ASNFZ4mrTe-hI0VniavN7w' as Base64url)).toEqual(
'01234567-89ab-4def-a123-456789abcdef'
);
// https://github.com/taskcluster/slugid/blob/v3.0.0/slugid_test.js#L37-L38
expect(toBase64url('804f3fc8-dfcb-4b06-89fb-aefad5e18754' as UUID)).toEqual(
'gE8_yN_LSwaJ-6761eGHVA'
);
expect(fromBase64url('gE8_yN_LSwaJ-6761eGHVA' as Base64url)).toEqual(
'804f3fc8-dfcb-4b06-89fb-aefad5e18754'
);
});
test('toBase64url() with 00000000-0000-0000-0000-000000000000', () => {
expect(toBase64url('00000000-0000-0000-0000-000000000000' as UUID)).toEqual(
'AAAAAAAAAAAAAAAAAAAAAA'
);
});
test('toBase64url() with invalid UUID', () => {
expect(() => toBase64url('invalid' as UUID)).toThrow("Invalid UUID: 'invalid'");
});
test('toBase64url() with undefined', () => {
expect(() => toBase64url(undefined as any)).toThrow("Invalid UUID: 'undefined'");
});
test('fromBase64url() with 00000000-0000-0000-0000-000000000000', () => {
expect(fromBase64url('AAAAAAAAAAAAAAAAAAAAAA' as Base64url)).toEqual(
'00000000-0000-0000-0000-000000000000'
);
});
test('fromBase64url() with invalid base64url', () => {
expect(() => fromBase64url('invalid' as Base64url)).toThrow(
'The string to be decoded contains invalid characters'
);
});
test('fromBase64url() with undefined', () => {
expect(() => fromBase64url(undefined as any)).toThrow(
"Cannot read properties of undefined (reading 'replace')"
);
});
test('convert 3a34ea98-651e-4253-92af-653373a20c51 <=> OjTqmGUeQlOSr2Uzc6IMUQ', () => {
const uuid = '3a34ea98-651e-4253-92af-653373a20c51' as UUID;
const base64 = toBase64(uuid);
expect(base64).toEqual('OjTqmGUeQlOSr2Uzc6IMUQ==');
expect(fromBase64(base64)).toEqual(uuid);
const base64url = toBase64url(uuid);
expect(base64url).toEqual('OjTqmGUeQlOSr2Uzc6IMUQ');
expect(fromBase64url(base64url)).toEqual(uuid);
});
test('convert back and forth base64url to UUID 1000x', () => {
for (let i = 0; i < 1000; i++) {
const uuid = dangerouslyGenerate();
const base64 = toBase64(uuid);
const base64url = toBase64url(uuid);
expect(fromBase64(base64)).toEqual(uuid);
expect(fromBase64url(base64url)).toEqual(uuid);
}
});
test.skip('vs slugid npm package 1000x', () => {
for (let i = 0; i < 1000; i++) {
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const slug = slugid.v4();
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const uuid = slugid.decode(slug);
expect(toBase64url(uuid)).toEqual(slug);
expect(fromBase64url(slug)).toEqual(uuid);
}
});
test('dangerouslyGenerate()', () => {
const mathRandomSpy = jest.spyOn(Math, 'random').mockImplementation(() => 0);
const uuid = dangerouslyGenerate();
expect(uuid).toEqual('00000000-0000-4000-8000-000000000000');
expect(mathRandomSpy).toHaveBeenCalledTimes(31);
mathRandomSpy.mockRestore();
});
import { Opaque } from 'type-fest';
// [Why Auto Increment Is A Terrible Idea](https://www.clever-cloud.com/blog/engineering/2015/05/20/why-auto-increment-is-a-terrible-idea/)
export type UUID = Opaque<string, 'UUID'>;
// base64url: 'AAAAAAAAAAAAAAAAAAAAAA'
export const zeroUUID = '00000000-0000-0000-0000-000000000000' as UUID;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment