Skip to content
darthcavPublic

About

Typescript utility functions

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

@darthcav/ts-utils

Node Version CI Coverage

A collection of utility functions for TypeScript applications and modules.

Documentation

API Documentation

Features

  • main: bootstraps a Node.js process with startup logging, signal and error handlers, log flushing on exit, and optional memory monitoring.
  • getConsoleLogger and getDummyLogger: a configured LogTape console logger, and a no-op logger for tests.
  • monitorMemory: periodic uptime and memory usage reports.
  • millisecondsToString: human-readable, localized durations.
  • osRelease: OS name, version, and architecture on Linux and Windows.
  • RuntimeObject with isRuntimeObject, isString, asRuntimeObject, asString, and toRuntimeObjectArray: narrow unknown values such as parsed JSON without type assertions.
  • Assert, Equal, Extends, Not, IsAny, IsNever, and IsUnknown: type-level assertions for tests.
  • noop: a function that does nothing.

ESM only, with TypeScript declarations included; requires Node.js 26 or later.

Installation

npm install @darthcav/ts-utils

Usage

getConsoleLogger

Configures logging and resolves to a Logger for the given category name. Records at or above lowestLevel are written to the console with RFC 3339 timestamps. The internal logtape/meta logger is silenced. The function is safe to call more than once — each call reconfigures logtape from scratch (the most recent call wins).

ANSI colors are used only when both stdout and stderr are terminals that support them (respecting NO_COLOR, NODE_DISABLE_COLORS, and TERM=dumb), so logs redirected to files or pipes stay free of escape codes. Set FORCE_COLOR to override: 0 or false disables colors, any other value enables them.

import { getConsoleLogger, main } from "@darthcav/ts-utils"

const logger = await getConsoleLogger("my-app")

main("my-app", logger, {
    launcher: () => {
        logger.info(`Application is running`)
        // start servers, connect to databases, etc.
    },
})

Pass a second argument to change the minimum log level (defaults to "info"):

const logger = await getConsoleLogger("my-app", "debug")

monitorMemory

Starts a periodic interval that logs process uptime and memory usage (in bytes) through a "monitorMemory" child category of the given logger. The interval defaults to every 24 hours and must be between one minute (1 / 60) and about 596.5 hours (2³¹ − 1 ms, the longest delay setInterval supports); anything outside that range throws a RangeError.

It returns a function that stops the monitoring. The timer is unreferenced, so monitoring alone does not keep the process alive. Besides the human-readable message, each report attaches its figures as structured properties (uptime and uptimeMs; rss, heapTotal, heapUsed, and external), so sinks that keep properties, such as JSON Lines, can query them.

import { getConsoleLogger, main, monitorMemory } from "@darthcav/ts-utils"

const logger = await getConsoleLogger("my-app")

main("my-app", logger, {
    launcher: () => {
        monitorMemory(logger)                  // every 24 hours
        const stop = monitorMemory(logger, 1)  // every hour
        // later, e.g. during shutdown:
        stop()
    },
})

millisecondsToString

Converts a duration in milliseconds to a human-readable string via Intl.DurationFormat. Milliseconds are shown as their own integer component (fractional inputs are rounded to the nearest millisecond), and any zero-valued components are omitted — so a zero-millisecond input returns an empty string. Negative durations are formatted from their magnitude and prefixed with "-". An optional BCP 47 locale tag (default "en") controls the unit labels. Throws a RangeError if ms is not finite or locale is not a valid BCP 47 tag.

import { millisecondsToString } from "@darthcav/ts-utils"

millisecondsToString(3_661_000)        // "1h 1m 1s"
millisecondsToString(90_000)           // "1m 30s"
millisecondsToString(5_250)            // "5s 250ms"
millisecondsToString(499)              // "499ms"
millisecondsToString(-90_000)          // "-1m 30s"
millisecondsToString(90_061_000, "es") // "1d 1h 1min 1s"

noop

A no-op function that does nothing and returns void. Useful as a placeholder callback or default handler.

import { noop } from "@darthcav/ts-utils"

setTimeout(noop, 1000)
element.addEventListener("click", noop)

RuntimeObject

Represents an object whose keys and values are only known at runtime. It is defined as Record<string, unknown> so values must be narrowed before use.

import type { RuntimeObject } from "@darthcav/ts-utils"

const payload: RuntimeObject = {
    id: 123,
    metadata: { active: true },
}

Type guards and narrowing helpers

Narrow unknown values (e.g. parsed JSON) without type assertions:

  • isRuntimeObject / isString are type guards for use in conditions.
  • asRuntimeObject / asString return the value, or undefined when it does not match, for use with ?. and ??.
  • toRuntimeObjectArray keeps only the plain-object elements of an array.

A runtime object is a plain object (an object literal, a JSON.parse result, or an Object.create(null) object); arrays, class instances, and built-ins such as Date or Map are rejected. Keys are not sanitized: an own __proto__ or constructor key from untrusted JSON is kept, so guard against those keys before merging the value into other objects.

import {
    asRuntimeObject,
    asString,
    isRuntimeObject,
    isString,
    toRuntimeObjectArray,
} from "@darthcav/ts-utils"

const body: unknown = JSON.parse(input)

if (isRuntimeObject(body) && isString(body["name"])) {
    console.log(body["name"].toUpperCase()) // narrowed to string
}

const name = asString(asRuntimeObject(body)?.["name"]) ?? "anonymous"
const items = toRuntimeObjectArray(asRuntimeObject(body)?.["items"])

osRelease

Returns OS release information for the current platform as an OsRelease object, or null on unsupported platforms or when no os-release file can be read on Linux (it never throws for a missing or unreadable file).

The OsRelease type exposes three normalized fields — name, version, and arch — present on all platforms. On Linux, the data comes from /etc/os-release, falling back to /usr/lib/os-release, and all raw key-value pairs (e.g. PRETTY_NAME, ID, ID_LIKE) are also accessible by string index. Comment lines are ignored, single- and double-quoted values are unquoted, and keys that are not upper-case shell variable names (such as __proto__) are skipped.

import { osRelease } from "@darthcav/ts-utils"

const info = osRelease()
if (info) {
    console.log(info.name)        // e.g. "Ubuntu" or "Windows 11"
    console.log(info.version)     // e.g. "24.04" or "10.0.22000"
    console.log(info.arch)        // e.g. "x64"
    console.log(info.PRETTY_NAME) // e.g. "Ubuntu 24.04 LTS" (Linux only)
}

getDummyLogger

Returns a no-op Logger (synchronously) useful as a placeholder in tests. All logging methods are no-ops and isEnabledFor always returns false. getChild and with return the same dummy logger instance. Each call returns a new instance, so mocking a method on one does not affect others.

import { getDummyLogger } from "@darthcav/ts-utils"

const logger = getDummyLogger()
// use logger in tests without any console output

main

Bootstraps an application process: logs startup information, optionally registers handlers for SIGINT and SIGTERM, always registers handlers for uncaughtException and unhandledRejection, optionally starts memory monitoring, then runs an optional launcher function.

The process title, PID, name, and NODE_ENV are logged at info level. The Node.js process options (execArgv and NODE_OPTIONS) are logged at debug level only, since they can reveal sensitive flags such as --inspect=0.0.0.0.

Before exiting, main flushes and disposes the configured logtape sinks (for at most flushTimeoutMs, 3 seconds by default), so buffered or asynchronous sinks keep the final messages. On SIGINT/SIGTERM it logs the signal at info level and then re-raises it, so the process ends as killed by that signal (exit status 130/143), which shells and supervisors such as systemd treat as a clean stop. If another listener for that signal is registered, the process exits with status 128 + signal number instead. Uncaught exceptions, unhandled rejections, and launcher failures exit with status 1. A second signal or error during the flush exits immediately.

Optional settings are passed as a MainOptions object:

Option Type Default Description
launcher () => void | Promise<void> — Runs after the handlers are registered; may be async.
monitorMemoryHours number 0 Logs memory usage every N hours; 0 disables it.
defaultInterruptionHandler boolean true Register the SIGINT/SIGTERM handlers.
flushTimeoutMs number 3000 Longest wait for the log sinks to flush (0 to 2³¹ − 1 ms).

An out-of-range monitorMemoryHours (negative, NaN, or outside monitorMemory's range) or flushTimeoutMs throws a RangeError before any handler is registered. If the launcher throws or its promise rejects, the failure is logged and the process exits with status 1.

main returns a teardown function that removes the handlers it registered and stops the memory monitoring (it does not exit the process). Only one main call can be active at a time: calling it again before the teardown throws, instead of registering a second set of handlers.

import { getLogger } from "@logtape/logtape"
import { main } from "@darthcav/ts-utils"

const logger = getLogger(["my-app"])

// Call main() once per process. Alternatives:

// 1. All defaults:
main("my-app", logger)

// 2. An async launcher and memory logging every 2 hours:
main("my-app", logger, {
    launcher: async () => {
        await connectToDatabase()
        startServer()
    },
    monitorMemoryHours: 2,
})

// 3. Manage graceful shutdown in the application instead:
let server: Server | undefined
const teardown = main("my-app", logger, {
    launcher: () => {
        server = startServer()
    },
    defaultInterruptionHandler: false,
})
process.once("SIGTERM", async () => {
    await server?.close()
    teardown()
})

The positional forms of earlier versions (e.g. main("my-app", logger, () => startServer(), 2)) were removed in 0.11.0; passing anything other than an options object throws a TypeError.

Type testing

Type-level helpers to check types at compile time, e.g. in test files. They are types only, so they add no code at runtime:

  • Assert<T> type-checks only when T is exactly true.
  • Equal<X, Y> is true when X and Y are identical types. It is stricter than mutual assignability: any only equals any, and readonly and ? modifiers must match.
  • Extends<X, Y> is true when X is assignable to Y (unions are compared as a whole).
  • Not<T> negates a predicate result.
  • IsAny<T>, IsNever<T>, and IsUnknown<T> detect those special types.

Declare each assertion as an (unused) type alias; a failing one is reported by tsc (e.g. tsc --noEmit), not by the test runner, since Node.js strips types without checking them. Use @ts-expect-error to assert that an assertion fails.

import {
    type Assert,
    asString,
    type Equal,
    type IsUnknown,
    type Not,
} from "@darthcav/ts-utils"

type _AsString = Assert<Equal<ReturnType<typeof asString>, string | undefined>>
type _NotAny = Assert<Not<Equal<ReturnType<typeof asString>, any>>>
type _Parsed = Assert<IsUnknown<Parameters<typeof asString>[0]>>
// @ts-expect-error -- `string` is not `number`
type _Fails = Assert<Equal<string, number>>

For the full API reference see the API Documentation.

Development

# Install dependencies
npm install

# Type-check
npm run typecheck

# Build (compile to JavaScript)
npm run build

# Run tests
npm test

# Lint and format
npm run lint
npm run lint:fix

# Generate documentation
npm run doc

Project Structure

src/
  index.ts                 # Public API entry point
  main.ts                  # Process bootstrap (main)
  millisecondsToString.ts  # Duration formatting
  monitorMemory.ts         # Periodic memory reporting
  noop.ts                  # No-op function
  osRelease.ts             # OS release utilities
  types.ts                 # RuntimeObject type and type guards
  loggers/                 # Logger utilities
  type-testing/            # Type-level assertion helpers
  __tests__/               # Test files
dist/                      # Compiled output (generated)
public/                    # Documentation output (generated)

License

Apache-2.0

About

Typescript utility functions

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages