UUIDはBase58に変換することで短く、可読性の高いIDにできる

UUIDとは?

UUID(Universally Unique Identifier)とはコンピュータシステム内で、リソースを一意に識別するために使用される128ビットの数値です。

JavaScriptではcryptoモジュールのrandomUUID()メソッドを使用して生成できます。

JavaScript
crypto.randomUUID()
// 8348fc20-a079-4c19-926c-4e89a3944e52

crypto.randomUUID()で生成されるのはタイムスタンプが含まれないUUID v4なので、タイムスタンプが含まれるUUIDを生成したい場合はUUID v7を使用します。

Base58で短く、可読性の高いIDにする

UUIDは通常36文字ですが、Base58でエンコードすると約22文字になります。

また、「0(ゼロ)・O(オー)・I(アイ)・l(エル)」など混同しやすい文字を除外しているため、読み間違いを防げます。

JavaScriptでライブラリを使用せずにBase58でデコード・エンコードするには、以下のようにします。

UUIDの形式だと直接Base58でデコード・エンコードできないので、バイトの変換処理も行っています。

JavaScript
const uuid = '8348fc20-a079-4c19-926c-4e89a3944e52' // or crypto.randomUUID()
const base58Chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
const base58Map = Object.fromEntries([...base58Chars].map((c, i) => [c, i]))

function uuidToBytes(uuid) {
  const hex = uuid.replace(/-/g, '')
  const bytes = new Uint8Array(hex.length / 2)
  for (let i = 0; i < bytes.length; i++) {
    bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16)
  }
  return bytes
}

function bytesToUuid(bytes) {
  const hex = [...bytes].map(b => b.toString(16).padStart(2, '0')).join('')
  return [
    hex.slice(0, 8),
    hex.slice(8, 12),
    hex.slice(12, 16),
    hex.slice(16, 20),
    hex.slice(20)
  ].join('-')
}

// UUIDをBase58でエンコード
function encodeBase58(bytes) {
  let num = BigInt('0x' + [...bytes].map(b => b.toString(16).padStart(2, '0')).join(''))
  let result = ''
  while (num > 0n) {
    const remainder = num % 58n
    result = base58Chars[Number(remainder)] + result
    num = num / 58n
  }

  for (const byte of bytes) {
    if (byte === 0) result = '1' + result
    else break
  }

  return result
}

function decodeBase58(str) {
  let num = 0n
  for (const char of str) {
    const val = base58Map[char]
    if (val === undefined) throw new Error(`Invalid Base58: ${char}`)
    num = num * 58n + BigInt(val)
  }

  let hex = num.toString(16)
  if (hex.length % 2) hex = '0' + hex
  const leadingZeros = str.match(/^1+/)?.[0].length ?? 0
  const zeros = '00'.repeat(leadingZeros)
  const fullHex = zeros + hex

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

  return bytes
}

const uuidBytes = uuidToBytes(uuid)
const uuidBase58 = encodeBase58(uuidBytes)
console.log('Base58:', uuidBase58)
// Base58: DGyBmnnQUGNrS7dKU579T

const decodedBytes = decodeBase58(uuidBase58)
const decodeUuid = bytesToUuid(decodedBytes)
console.log('decode UUID:', decodeUuid)
// decode UUID: 8348fc20-a079-4c19-926c-4e89a3944e52
console.log(uuid === decodeUuid) // true

前述のコードからBase58にエンコード後は「DGyBmnnQUGNrS7dKU579T」と短くなっていることと、デコードで「8348fc20-a079-4c19-926c-4e89a3944e52」に戻せることが確認できます。
ライブラリを使用する場合は、bs58を「npm i bs58」でインストールして、以下のように使用します。

JavaScript
import bs58 from 'bs58'
const uuid = '8348fc20-a079-4c19-926c-4e89a3944e52' // or crypto.randomUUID()

function uuidToBytes(uuid) {
  const hex = uuid.replace(/-/g, '')
  const bytes = new Uint8Array(hex.length / 2)
  for (let i = 0; i < bytes.length; i++) {
    bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16)
  }
  return bytes
}

function bytesToUuid(bytes) {
  const hex = [...bytes].map(b => b.toString(16).padStart(2, '0')).join('')
  return [
    hex.slice(0, 8),
    hex.slice(8, 12),
    hex.slice(12, 16),
    hex.slice(16, 20),
    hex.slice(20)
  ].join('-')
}

// UUIDをBase58でエンコード
const uuidBytes = uuidToBytes(uuid)
const base58Encoded = bs58.encode(uuidBytes)
console.log('Base58:', base58Encoded)
// Base58: DGyBmnnQUGNrS7dKU579T

// Base58から元のUUIDにデコード
const decodedBytes = bs58.decode(base58Encoded)
const decodedUuid = bytesToUuid(decodedBytes)
console.log('decode UUID:', decodedUuid)
// decode UUID: 8348fc20-a079-4c19-926c-4e89a3944e52
console.log(uuid === decodedUuid) // true

UUIDをBase58に変換するライブラリ

先週@nakanoaas/uuid58というUUIDをBase58に直接変換できるライブラリが公開されたので、こちらを使用するとバイト変換の関数を別途作成する必要がなくなるので便利です。

@nakanoaas/uuid58

JavaScript
import { uuid58, uuid58Decode, uuid58Encode } from "@nakanoaas/uuid58";

// Generate a new Base58-encoded UUID (always 22 characters)
const id = uuid58();
// => "XDY9dmBbcMBXqcRvYw8xJ2" (22 characters)

// Convert existing UUID to Base58 (always 22 characters)
const encoded = uuid58Encode("f4b247fd-1f87-45d4-aa06-1c6fc0a8dfaf");
// => "XDY9dmBbcMBXqcRvYw8xJ2" (22 characters)

// Convert Base58 back to UUID
const decoded = uuid58Decode("XDY9dmBbcMBXqcRvYw8xJ2");
// => "f4b247fd-1f87-45d4-aa06-1c6fc0a8dfaf"

ただし、@nakanoaas/uuid58のuuid58()はcrypto.randomUUID()で生成しているので、UUID v7で生成したい場合はuuidv7を使用してください。

まとめ

  • JavaScriptではcrypto.randomUUID() によってUUID v4を生成できます。
  • UUID v4はタイムスタンプを含まないため、時系列の管理などが必要な場合はUUID v7の使用が適しています。
  • UUIDは通常36文字ですが、Base58でエンコードすると約22文字になり、視認性・可読性が向上します。
  • Base58は「0」「O」「I」「l」など紛らわしい文字を除外するため、コピーや読み取りミスの防止にも有効です。
  • JavaScriptでBase58エンコード・デコードを簡単に扱うには@nakanoaas/uuid58のライブラリの利用が便利です。