Created
January 30, 2026 14:16
-
-
Save tkrotoff/e7a098557616e1d7186b2b7c0fa3112d to your computer and use it in GitHub Desktop.
Encode/decode UUID to Base64
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* 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; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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'>; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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