This project has moved to
@pimasi/utils. This repository is no longer maintained.
npm install @bahatron/utilsThe only utility library you'll ever need - a comprehensive collection of TypeScript utilities for common programming tasks.
-
typeboxdependency removed — all schema types (TSchema,Static, etc.) are now built-in. Thetypeboxpackage is no longer a dependency. -
Typesexport removed —Types.Resolved<T>,Types.Falsy, andTypes.Truthyare no longer exported. Use TypeScript's built-inAwaited<T>instead ofResolved<T>. -
JsonSchemarestructured — the top-level export is stillJsonSchema, but it now contains aSchemaobject with all builders andvalidate. Type helpers (Static,InferRawSchema) are exported at theJsonSchemalevel. -
Logger.Createrenamed toLogger.Logger— the default export was removed in favour of a named export. -
Logger.Formattersrestructured —Formatters.Pretty(wasPrettyFormatter) andFormatters.Yml(wasYmlFormatter) are now named exports under aFormattersnamespace. -
Schema builder API replaced — TypeBox-based helpers (
StringEnum,Nullable,Email,DateExtended) are replaced by a new built-in schema system withSchema.Nullable(),Schema.Optional()wrappers andenumoptions on primitive builders.Compositereplaces manualallOfcomposition.
// ─── Imports ─────────────────────────────────────────────────────────────────
// v4
import { JsonSchema, Types, Logger } from "@bahatron/utils";
JsonSchema.StringEnum(["a", "b"] as const);
JsonSchema.Nullable(JsonSchema.String());
type T = Types.Resolved<Promise<string>>;
const logger = Logger.Create({ id: "app" });
Logger.Formatters.Pretty(entry);
// v5
import { JsonSchema, Logger } from "@bahatron/utils";
const { Schema } = JsonSchema;
Schema.String({ enum: ["a", "b"] as const }); // "a" | "b"
Schema.Nullable(Schema.String()); // string | null
type T = Awaited<Promise<string>>; // built-in TS utility
const logger = Logger.Logger({ id: "app" });
Logger.Formatters.Pretty(entry);
// ─── Schema builders ────────────────────────────────────────────────────────
// v4 (TypeBox)
import { JsonSchema } from "@bahatron/utils";
JsonSchema.String();
JsonSchema.StringEnum(["a", "b"] as const);
JsonSchema.Nullable(JsonSchema.String());
JsonSchema.Email();
// v5
const { Schema } = JsonSchema;
Schema.String();
Schema.String({ enum: ["a", "b"] as const });
Schema.Nullable(Schema.String());
Schema.String({ format: "email" });
// ─── Validation ─────────────────────────────────────────────────────────────
// v4
JsonSchema.validate(data, schema);
// v5
Schema.validate(data, schema);Fast and simple event-driven logger with customizable formatting and log level control.
- Event-driven architecture for async processing
- Multiple log levels: DEBUG, INFO, WARNING, ERROR
- Customizable formatters (JSON or pretty-print)
- Dynamic logger ID via function or string
- Non-blocking async handlers
- Minimum log level filtering
- Symbol and circular reference serialization in context
import { Logger } from "@bahatron/utils";
const logger = Logger.Logger({
id: "myLogger",
pretty: false,
minLogLevel: "INFO",
});
// Log messages
logger.debug({ userId: 123 }, "User action");
logger.info("Application started");
logger.warn({ retry: 3 }, "Retrying operation");
logger.error(new Error("Something went wrong"), "Operation failed");
// Listen to log events
const handler = logger.on("ERROR", async (entry) => {
// Send to error tracking service
await sendToSentry(entry);
});
// Wait for all async handlers to complete
await logger.flush();
// Remove handler
logger.off("ERROR", handler);import { Logger } from "@bahatron/utils";
Logger.Formatters.Pretty(entry); // Human-readable coloured output
Logger.Formatters.Yml(context); // YAML-like indented formatLogger.Logger(options?)- Create a new logger instancelogger.debug(context, message?)- Log debug informationlogger.info(context, message?)- Log informational messageslogger.warn(context, message?)- Log warningslogger.error(err, message?)- Log errorslogger.on(level, handler)- Attach async handler to log levellogger.off(level, handler)- Remove handlerlogger.flush()- Wait for all pending async handlers
Collection of highly useful helper functions for common tasks.
Execute tasks in parallel with controlled concurrency using a worker pool.
import { Helpers } from "@bahatron/utils";
const urls = ["url1", "url2", "url3", ...];
await Helpers.parallelize({
workers: 10,
queue: urls,
handler: async (url) => {
const data = await fetch(url);
// process data
},
});Retry a function with exponential backoff on failure.
import { Helpers } from "@bahatron/utils";
const result = await Helpers.retry(
async () => {
return await unstableApiCall();
},
{
tries: 5, // Number of retry attempts (default: 3)
baseDelay: 1000, // Base timeout in ms (default: 0)
factor: 2, // Exponential backoff factor (default: 1)
},
);Safely retrieve environment variables with optional default values.
import { Helpers } from "@bahatron/utils";
// With default value
const apiKey = Helpers.getenv("API_KEY", "default-key");
// Without default (throws InternalError if not set)
const dbUrl = Helpers.getenv("DATABASE_URL");Pause execution for a specified duration.
import { Helpers } from "@bahatron/utils";
await Helpers.sleep(2000); // Sleep for 2 secondsSafe JSON stringification with circular reference handling using fast-safe-stringify.
import { Helpers } from "@bahatron/utils";
const circularObj = { a: 1 };
circularObj.self = circularObj;
const json = Helpers.jsonStringify(circularObj);
// Returns safe JSON string instead of throwingSafe JSON parsing that returns undefined instead of throwing errors.
import { Helpers } from "@bahatron/utils";
const obj = Helpers.jsonParse('{"valid": "json"}');
// Returns: { valid: "json" }
const invalid = Helpers.jsonParse("invalid json");
// Returns: undefined (instead of throwing)Wrapper for command-line scripts with proper error handling and process exit codes.
import { Helpers } from "@bahatron/utils";
Helpers.execute(async () => {
// Your script logic
await doSomething();
// Exits with code 0 on success, 1 on error
});Type-safe JSON Schema builder with full TypeScript inference — no external schema library required.
- Zero-dependency schema builders (
String,Number,Boolean,Array,Object,Const,Any,Record,Composite,AnyOf,OneOf,Recursive) - Wrappers:
Nullable,Optional,Required,Pick,Omit - Full TypeScript type inference via
Static<T>branded types - Raw
as constschema inference viaInferRawSchema<T> Schema.From()for inferring schemas from non-as constobjects and JSON importsenumoption on primitive builders,$id,$schema,description,titlemetadata on every builder- Runtime validation using jsonschema
import { JsonSchema } from "@bahatron/utils";
const { Schema } = JsonSchema;
// Type helpers
type MyType = JsonSchema.Static<typeof MySchema>;
type RawInferred = JsonSchema.InferRawSchema<typeof rawSchema>;Schema.String(); // string
Schema.Nullable(Schema.String()); // string | null
Schema.String({ enum: ["a", "b"] as const }); // "a" | "b"
Schema.String({ format: "email" }); // string (validated as email)
Schema.Number(); // number
Schema.Number({ enum: [1, 2, 3] as const }); // 1 | 2 | 3
Schema.Number({ integer: true }); // integer (no decimals)
Schema.Number({ integer: true, minimum: 1 }); // integer with constraints
Schema.Boolean(); // boolean
Schema.Nullable(Schema.Boolean()); // boolean | nullSchema.Const("active"); // "active"
Schema.Const(42); // 42Schema.Any(); // any
Schema.Any({ description: "metadata" }); // any (with description)Schema.Array(Schema.String()); // string[]
Schema.Nullable(Schema.Array(Schema.Number())); // number[] | nullconst UserSchema = Schema.Object({
id: Schema.Number(),
name: Schema.String(),
role: Schema.String({ enum: ["admin", "user"] as const }),
bio: Schema.Optional(Schema.Nullable(Schema.String())),
});
type User = JsonSchema.Static<typeof UserSchema>;
// { id: number; name: string; role: "admin" | "user"; bio?: string | null }Merges multiple object schemas into one. Handles nullable source schemas. Also accepts Schema.OneOf / Schema.AnyOf unions (whose members are all objects) — these are preserved and combined via JSON Schema allOf.
const BaseSchema = Schema.Object({
id: Schema.Number(),
createdAt: Schema.String(),
});
const UserSchema = Schema.Composite([
BaseSchema,
Schema.Object({
name: Schema.String(),
email: Schema.String(),
}),
]);
// { id: number; createdAt: string; name: string; email: string }
// Composite with OneOf — union branches are kept in allOf
const ByPostcode = Schema.Object({ postcode: Schema.String() });
const ByLatLon = Schema.Object({ lat: Schema.Number(), lon: Schema.Number() });
const SearchSchema = Schema.Composite([
Schema.Object({ bedrooms: Schema.Number() }),
Schema.OneOf([ByPostcode, ByLatLon]),
]);
// { bedrooms: number } & ({ postcode: string } | { lat: number; lon: number })const StatusSchema = Schema.AnyOf([Schema.String(), Schema.Number()]);
// string | number
const EventSchema = Schema.OneOf(
[
Schema.Object({ type: Schema.Const("click"), x: Schema.Number() }),
Schema.Object({ type: Schema.Const("key"), code: Schema.String() }),
],
{ discriminator: { propertyName: "type" } },
);
// { type: "click"; x: number } | { type: "key"; code: string }Define self-referencing schemas using $ref. The schema is automatically registered for validation.
type TreeNode = { value: string; children: TreeNode[] };
const TreeSchema = Schema.Recursive<TreeNode>("TreeNode", (self) =>
Schema.Object({
value: Schema.String(),
children: Schema.Array(self),
}),
);
// { value: string; children: TreeNode[] }Schema.Record(Schema.String(), Schema.Number()); // Record<string, number>
Schema.Record(Schema.Number(), Schema.String()); // Record<number, string>
// Key with pattern — uses the regex directly as the patternProperties key
Schema.Record(Schema.String({ pattern: "^[a-z]+$" }), Schema.Any());
// Key with format — maps known formats (email, uuid, date-time, etc.) to regex
Schema.Record(Schema.String({ format: "email" }), Schema.Any());
// Integer key — uses integer-only regex (no decimals)
Schema.Record(Schema.Number({ integer: true }), Schema.String());Schema.Nullable(Schema.String()); // string | null
Schema.Optional(Schema.String()); // string (optional in parent object)
Schema.Required(Schema.Nullable(Schema.String())); // string (strips null + optional)const User = Schema.Object({
name: Schema.String(),
age: Schema.Number(),
email: Schema.String(),
});
Schema.Pick(User, ["name", "email"]); // { name: string; email: string }
Schema.Omit(User, ["email"]); // { name: string; age: number }Infer a typed schema from a plain object (e.g. a JSON import). No as const needed — the type is inferred via a const generic parameter.
const schema = Schema.From({
type: "object",
properties: {
name: { type: "string" },
age: { type: "number" },
},
required: ["name", "age"],
});
type T = JsonSchema.Static<typeof schema>; // { name: string; age: number }Use InferRawSchema to extract TypeScript types directly from as const JSON Schema objects — no builder functions needed:
const schema = {
type: "object",
properties: {
id: { type: "number" },
name: { type: "string" },
role: { enum: ["admin", "user"] },
active: { const: true },
},
required: ["id", "name", "role", "active"],
} as const;
type MyType = JsonSchema.InferRawSchema<typeof schema>;
// { id: number; name: string; role: "admin" | "user"; active: true }Supported keywords: type, const, enum, properties/required, items, anyOf, oneOf, nullable.
Schema.validate returns the typed object if valid, or throws ValidationFailed with details.
const UserSchema = Schema.Object({
id: Schema.Number(),
name: Schema.String(),
});
// Returns typed object
const user = Schema.validate({ id: 1, name: "John" }, UserSchema);
user.id; // number
user.name; // string
// Throws on invalid data
try {
Schema.validate({ id: "bad" }, UserSchema);
} catch (err) {
// err is ValidationFailed with error details
console.error(err.context);
}Builders — accept an options object with $id?, $schema?, description?, title?:
Schema.Const(value, opts?)— literal value schemaSchema.String(opts?)— string schema (supportsenum,format,minLength,maxLength,pattern)Schema.Number(opts?)— number schema (supportsenum,minimum,maximum,multipleOf)Schema.Boolean(opts?)— boolean schemaSchema.Any(opts?)— any value (empty schema)Schema.Array(items, opts?)— array schema (supportsminItems,maxItems,uniqueItems)Schema.Object(properties, opts?)— object schema (supportsadditionalProperties)Schema.Record(keySchema, valueSchema)— record / dictionary schemaSchema.Composite(schemas[], opts?)— merge multiple object schemas (also acceptsOneOf/AnyOfunions of objects)Schema.AnyOf(schemas[], opts?)— union (any match)Schema.OneOf(schemas[], opts?)— union (exactly one match, supportsdiscriminator)Schema.Recursive(id, builder)— self-referencing schema via$refSchema.From(raw)— infer typed schema from a plain object
Wrappers:
Schema.Nullable(schema)— make schema acceptnullSchema.Optional(schema)— make property optional in parent objectSchema.Required(schema)— stripnulland optional from schemaSchema.Pick(objectSchema, keys[])— keep only selected keysSchema.Omit(objectSchema, keys[])— remove selected keys
Validation:
Schema.validate(data, schema)— validate and return typed data, or throwValidationFailed
Runtime-agnostic, type-safe implementation of event emitter pattern.
- Type-safe event names and payloads
- Handler registration and removal
- One-time event handlers
- Global event listeners
- No dependencies on Node.js EventEmitter
import { Observable } from "@bahatron/utils";
// Define events with types
type Events = "userCreated" | "userDeleted" | "userUpdated";
type Payload = { userId: number; timestamp: Date };
const events = Observable<Events, Payload>();
// Listen to specific event
events.on("userCreated", (payload) => {
console.log(`User ${payload.userId} created`);
});
// Listen once
events.once("userDeleted", (payload) => {
console.log(`User ${payload.userId} deleted`);
});
// Listen to all events
events.onEvent((event, payload) => {
console.log(`Event: ${event}`, payload);
});
// Emit events
events.emit("userCreated", { userId: 123, timestamp: new Date() });
// Remove listener
const handler = (payload) => console.log(payload);
events.on("userUpdated", handler);
events.off("userUpdated", handler);on(event, handler)- Register event handleronce(event, handler)- Register one-time handleronEvent(handler)- Register global handler for all eventsoff(event, handler)- Remove event handleremit(event, payload?)- Emit event with optional payload
Pre-configured HTTP exception classes for common error scenarios.
All exceptions include:
name- Error namemessage- Error messagestatusCode/code- HTTP status codecontext- Additional error context
import { Error } from "@bahatron/utils";
// 4xx Client Errors
throw Error.BadRequest({ field: "email" }, "Invalid email format");
throw Error.ValidationFailed({ errors: [...] });
throw Error.Unauthorized();
throw Error.PaymentRequired();
throw Error.Forbidden();
throw Error.NotFound({ resource: "user", id: 123 });
throw Error.NotAcceptable();
throw Error.TimeOut();
throw Error.Conflict({ reason: "duplicate email" });
throw Error.Gone();
throw Error.PreconditionFailed();
throw Error.PayloadTooLarge();
throw Error.URITooLong();
throw Error.UnsupportedMediaType();
throw Error.ExpectationFailed();
throw Error.UnprocessableEntity();
throw Error.TooManyRequests();
// 5xx Server Errors
throw Error.InternalError(err, "Database connection failed");
throw Error.NotImplemented();import { Error } from "@bahatron/utils";
function getUser(id: number) {
const user = db.users.find(id);
if (!user) {
throw Error.NotFound({ userId: id }, "User not found");
}
return user;
}
try {
const user = getUser(123);
} catch (err) {
console.error(err.statusCode); // 404
console.error(err.message); // "User not found"
console.error(err.context); // { userId: 123 }
}Axios-compatible HTTP client built on native fetch. Drop-in replacement with the same API — create, interceptors, config merging, and error handling all work like axios.
- Built on native
fetch— zero HTTP dependencies - Axios-compatible API (
create, callable instances, interceptors) - Automatic JSON serialization/parsing
baseURL+ relative URL resolution- Query parameter serialization
- Request/response interceptors with
use/eject/clear - Configurable status validation
- Timeout support via
AbortController HttpErrorwithisAxiosError: truefor compatibility
import { Http } from "@bahatron/utils";
// Use the default instance directly
const response = await Http.http({
url: "https://api.example.com/users",
method: "get",
});
response.data; // parsed JSON
response.status; // 200
// Create a custom instance with defaults
const client = Http.http.create({
baseURL: "https://api.example.com",
headers: { Authorization: "Bearer token" },
timeout: 5000,
});
// Make requests — config merges with defaults
const user = await client({
url: "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/users/1",
method: "get",
});
// POST with JSON body (auto-serialized, Content-Type set automatically)
const created = await client({
url: "/users",
method: "post",
data: { name: "Rick", dimension: "C-137" },
});import { Http } from "@bahatron/utils";
const client = Http.create({ baseURL: "https://api.example.com" });
// Request interceptor — modify config before sending
const id = client.interceptors.request.use(
(config) => {
config.headers = {
...config.headers,
Authorization: "Bearer " + getToken(),
};
return config;
},
(error) => Promise.reject(error),
);
// Response interceptor — transform or handle responses
client.interceptors.response.use(
(response) => response,
(error) => {
if (error.status === 401) {
// handle unauthorized
}
return Promise.reject(error);
},
);
// Remove an interceptor
client.interceptors.request.eject(id);
// Remove all interceptors
client.interceptors.request.clear();import { Http } from "@bahatron/utils";
const client = Http.create({ baseURL: "https://api.example.com" });
try {
await client({ url: "/not-found" });
} catch (err) {
if (err instanceof Http.HttpError) {
err.status; // 404
err.message; // "Request failed with status code 404"
err.response; // { data, status, statusText, headers, config }
err.isAxiosError; // true (for compatibility)
}
}
// Accept all status codes (don't throw)
const response = await client({
url: "/not-found",
validateStatus: null,
});
response.status; // 404interface HttpRequestConfig<D = any> {
url?: string; // Request URL
method?: string; // HTTP method (default: "get")
baseURL?: string; // Prepended to relative URLs
headers?: Record<string, string>; // Request headers
params?: Record<string, string | number | boolean>; // Query parameters
data?: D; // Request body (auto-serialized to JSON)
timeout?: number; // Timeout in ms
validateStatus?: ((status: number) => boolean) | null; // Custom status validation
signal?: AbortSignal; // Abort signal
}interface HttpResponse<T = any, D = any> {
data: T; // Parsed response body
status: number; // HTTP status code
statusText: string; // HTTP status text
headers: Record<string, string>; // Response headers
config: HttpRequestConfig<D>; // Request config used
}Http.http(config)— make a request using the default instanceHttp.http.create(defaults?)— create a new instance with defaultsHttp.create(defaults?)— same as above (standalone export)instance(config)— make a requestinstance.defaults— instance default configinstance.interceptors.request.use(onFulfilled?, onRejected?)— add request interceptor, returns IDinstance.interceptors.response.use(onFulfilled?, onRejected?)— add response interceptor, returns IDinstance.interceptors.request.eject(id)— remove request interceptorinstance.interceptors.response.eject(id)— remove response interceptorinstance.interceptors.request.clear()— remove all request interceptorsinstance.interceptors.response.clear()— remove all response interceptors
Apache-2.0
Issues and pull requests are welcome at github.com/bahatron/utils