@inizioevoke/tsid
v1.0.0
Published
Timestamp-prefixed, lexicographically sortable unique IDs. Similar in spirit to [ULID](https://github.com/ulid/spec), with configurable output length and a monotonic factory.
Keywords
Readme
@inizioevoke/tsid
Timestamp-prefixed, lexicographically sortable unique IDs. Similar in spirit to ULID, with configurable output length and a monotonic factory.
Format
Every ID has the shape:
<10 timestamp chars><delimiter?><random chars>- Timestamp: 48 bits packed into 10 Crockford Base32 chars. Same width and encoding as ULID; the first char is always
0–7(top 2 bits unused). Range:0to281,474,976,710,655ms since epoch (year 10,889). - Random: variable width. Default is 30 bits (6 chars), giving 16-char IDs total.
- Delimiter: optional string between timestamp and random. Rejected if it contains any alphanumeric characters (would collide with the encoding).
Crockford Base32 excludes I, L, O, and U to reduce visual ambiguity. Because the alphabet is ASCII-ordered, lexicographic sort matches numeric sort — you can sort IDs as plain strings in a database column and get chronological order.
Install
npm install @inizioevoke/tsidEnvironment support
Uses the standard crypto.getRandomValues API for random generation, available on globalThis.crypto in:
- Node.js ≥ 20 (stable, unflagged).
- All modern browsers over
https://orlocalhost. Not available in insecure (http://) contexts on some browsers. - Deno, Bun, and other runtimes that expose Web Crypto.
One build serves every environment — the Quick start imports work identically in Node and bundlers (Vite, webpack, Rollup, esbuild). BigInt is used internally, so IE is not supported (Chrome 67+, Safari 14+, Firefox 68+).
Quick start
import { tsid, TSIDFactory, parseTSID } from '@inizioevoke/tsid';
tsid(); // '01M2GC9Y7C0JTWDS' — 16 chars
tsid({ bits: 128 }) // 128-bit ID, like ULID
tsid({ length: 26 }); // 26-char ID with more random entropy, like ULID
tsid({ delimiter: '-' }); // '01M2GC9Y7C-0JTWD'
tsid(1700000000000); // ID with a fixed timestamp
const factory = new TSIDFactory();
factory.generate(); // strictly increasing across calls
factory.generate();
parseTSID('01M2GC9Y7C0JTWDS'); // 1701213889388 (ms since epoch)tsid(...) — one-shot function
Each call creates a fresh factory and generates a single ID. Not monotonic across calls — if you need strict ordering, use TSIDFactory.
tsid(): string
tsid(timestamp: number): string
tsid(opts: TSIDOptions): string
tsid(timestamp: number, opts: TSIDOptions): string| Arg | Type | Default |
|---|---|---|
| timestamp | number (ms since epoch) | Date.now() |
| opts | TSIDOptions | {} |
TSIDFactory — monotonic generator
A stateful factory that guarantees every ID it produces is strictly greater than every previous ID from the same instance.
class TSIDFactory {
constructor();
constructor(timestamp: number);
constructor(opts: TSIDOptions);
constructor(timestamp: number, opts: TSIDOptions);
generate(): string;
}Constructor
Takes the same arguments as tsid(...). The timestamp (or Date.now() if omitted) is captured once and used as the anchor for every subsequent generate() call — the factory is decoupled from wall-clock time after construction.
generate()
- First call — anchors on the captured timestamp with a cryptographically random seed.
- Subsequent calls — hold the timestamp steady and advance the random portion by a random step in
[1, 2^stepBits], wherestepBits = min(randomBits / 2, 16). This keeps the sequence strictly monotonic while making the next ID non-trivial to predict. - Random space exhausted — timestamp bumps by 1 ms and the random portion rerolls with full entropy.
- Timestamp overflow — throws
Monotonic overflow: timestamp exceeded maximum(only reachable after generating an astronomically large number of IDs).
Example
const f = new TSIDFactory({ length: 11 });
f.generate(); // '01M2GCVAE0X'
f.generate(); // '01M2GCVAE0Y' — same timestamp, random advanced
f.generate(); // '01M2GCVAE0Z'
f.generate(); // '01M2GCVAE10' — random space exhausted, timestamp bumped
f.generate(); // '01M2GCVAE11'At length: 11 there's only 5 bits of random space, so overflow happens quickly. At the default 16 chars (30 random bits), roughly 65,000 IDs fit per ms before the timestamp bumps.
TSIDOptions
interface TSIDOptions {
bits?: number; // total ID bits (timestamp + random). Default: 78.
length?: number; // total output character count. Alternative to `bits`.
delimiter?: string; // inserted between timestamp and random. Default: ''.
}bitsvslength— set one or the other, not both.bitswins if both are given.lengthis more intuitive:length: 16gives a 16-char output. When bothlengthanddelimiterare set, the encoded portion shrinks to keep the total output at exactlylengthcharacters.- Minimum size — random must be at least 1 Crockford char (5 bits), so
bits >= 53andlength >= 11 + delimiter.length. - Delimiter rules — must not contain any alphanumeric characters (
[0-9A-Za-z]). Empty string is allowed and is the default.
Length ↔ bits reference
| length | bits (no delimiter) | Random chars | Random values |
|---|---|---|---|
| 11 | 53 | 1 | 32 |
| 12 | 58 | 2 | 1,024 |
| 16 (default) | 78 | 6 | ~1 billion |
| 20 | 98 | 10 | ~10²¹ |
| 24 | 118 | 14 | ~10²⁴ |
parseTSID(id)
Decodes the timestamp portion of an ID back to a millisecond epoch number.
parseTSID(id: string): number- Only reads the first 10 chars (the timestamp portion); the rest is ignored.
- Case-insensitive.
- Throws if the input is shorter than 10 chars or contains an invalid character.
parseTSID('01M2GC9Y7C0JTWDS'); // 1,701,213,889,388
parseTSID('01m2gc9y7c'); // 1,701,213,889,388 (lowercase OK)
parseTSID('OiM2GC9Y7C'); // invalid characters — throws errorSort ordering
IDs sort correctly as plain strings in any collation that compares by codepoint (SQL ORDER BY, Array.prototype.sort(), etc.):
const ids = [tsid(), tsid(), tsid()];
[...ids].sort(); // chronological orderThis holds because:
- The timestamp is fixed-width (10 chars, zero-padded).
- Crockford Base32's alphabet is ASCII-ordered.
- The random portion is fixed-width for a given
bits/length.
Script tag / IIFE build
For pages that consume the library via a <script> tag (no bundler, no module system), the package ships a self-contained IIFE bundle that installs a callable global named tsid.
Loading
<!-- production (minified) -->
<script src="https://unpkg.com/@inizioevoke/tsid/dist/iife/index.min.js"></script>
<!-- development (unminified, easier to debug) -->
<script src="https://unpkg.com/@inizioevoke/tsid/dist/iife/index.js"></script>
<!-- or served from a local install -->
<script src="./node_modules/@inizioevoke/tsid/dist/iife/index.min.js"></script>API
The IIFE global has a flatter, shorter shape than the module API. tsid itself is the generator function; parse and Factory are attached as properties.
| Module API | IIFE global |
| --------------------------------- | ------------------- |
| tsid(...) | tsid(...) |
| parseTSID(id) | tsid.parse(id) |
| new TSIDFactory(...) | new tsid.Factory(...) |
<script>
// Generator — same signatures as the module `tsid(...)` function
tsid(); // '01M2GC9Y7C0JTWDS'
tsid({ length: 20 }); // 20-char ID
tsid({ delimiter: '-' }); // '01M2GC9Y7C-0JTWD'
tsid(1700000000000); // ID with a fixed timestamp
// Parse — same as parseTSID
tsid.parse('01M2GC9Y7C0JTWDS'); // 1701213889388
// Monotonic factory — same as TSIDFactory
const f = new tsid.Factory({ length: 16 });
f.generate();
f.generate();
</script>TypeScript
The package publishes ambient type declarations for the tsid global. Pull them in project-wide via tsconfig:
{
"compilerOptions": {
"types": ["@inizioevoke/tsid/iife"]
}
}Or per-file with a triple-slash reference:
/// <reference types="@inizioevoke/tsid/iife" />
tsid();
tsid.parse('01M2GC9Y7C');
new tsid.Factory();The exported TSID type is also available if you need to reference the callable's shape directly:
import type { TSID } from '@inizioevoke/tsid/iife';Comparison to ULID
| | ULID | TSID |
|---|---|---|
| Encoding | Crockford Base32 | Crockford Base32 |
| Timestamp bits | 48 | 48 |
| Total length | 26 chars (fixed) | Configurable (default 16) |
| Random bits | 80 | Configurable (default 30) |
| Monotonic factory | Yes | Yes |
| Random step | +1 (standard impls) | random [1, 2^stepBits] |
| Delimiter | No | Optional |
TSID's key differentiator is length flexibility — pick 16 chars for URLs, 24+ for higher entropy needs — plus a random-step monotonic sequence that isn't linearly enumerable from a single sample.
