Minimal ecosystem-wide logging interface
Type-only contract for the functype ecosystem — four methods, no runtime, zero opinion on output format. One shared shape every functype-* package targets.
A minimal, ecosystem-wide logging interface. Type-only — zero runtime, no console dependency, no opinion on output format. Every present and future functype-* package targets this shape, so a consumer writes ONE logger adapter and it works everywhere.
export interface Logger {
debug(message: string, metadata?: Record<string, unknown>): void;
info(message: string, metadata?: Record<string, unknown>): void;
warn(message: string, metadata?: Record<string, unknown>): void;
error(message: string, metadata?: Record<string, unknown>): void;
}
Four methods, all mandatory. No trace/fatal/child/withContext in the core shape — richer loggers (like DirectLogger from functype-log) add those on top and remain structurally assignable.
In 1.3.x, Logger is reachable only from the functype/logger subpath:
import type { Logger } from "functype/logger";
Why subpath-only? Temporary workaround for a non-deterministic chunk-splitter bug in rolldown 1.1.0 (the bundler tsdown uses under the hood). Re-exporting
Loggerfrom the top-levelfunctypebarrel made the chunk graph dense enough that ~30% of downstream builds emitted broken chunks withCompanion$N is not definedreferences. The published1.3.0artifact on npm happens to expose both paths (it was built on a clean run), but subsequent 1.3.x releases will be subpath-only until rolldown ships a determinism fix. Future minor will restore barrel-parity.
If your application already has its own Logger type, rename on import:
import type { Logger as FunctypeLogger } from "functype/logger";
Logger is the only service-style interface in functype core — and only because every production TypeScript app already has one. The proposal explicitly rejected adding Clock, Random, or Tracer (framework abstractions Effect ships) on the same grounds: those aren’t universally needed the way logging is.
The proposal also rejected baking in a default implementation. Concrete loggers live in consumer packages (consoleBootLogger in functype-os/config, DirectLogger in functype-log). Core stays pure types — Bun/Deno/edge-runtime portability comes for free.
Any 4-method object with the right signatures satisfies Logger:
import type { Logger } from "functype/logger";
const myLogger: Logger = {
debug: (msg, meta) => console.debug(msg, meta ?? ""),
info: (msg, meta) => console.log(msg, meta ?? ""),
warn: (msg, meta) => console.warn(msg, meta ?? ""),
error: (msg, meta) => console.error(msg, meta ?? ""),
};
functype-logfunctype-log/direct exposes DirectLogger — a sync/imperative logger with debug/info/warn/error(msg, meta?): void methods plus extras (trace, fatal, withError, withContext, child). Its surface is a structural superset of core Logger, so it assigns directly with no adapter:
import { createDirectConsoleLogger } from "functype-log/direct";
import type { Logger } from "functype/logger";
const logger: Logger = createDirectConsoleLogger(); // no cast required
This lets you wire functype-log into any hook expecting a core Logger — including bootDiagnostics from functype-os/config:
import { bootDiagnostics, Layered, ProcessEnvSource } from "functype-os/config";
import { createDirectConsoleLogger } from "functype-log/direct";
bootDiagnostics({
source: Layered([ProcessEnvSource()]),
required: ["DATABASE_URL"],
logger: createDirectConsoleLogger(),
});
Note: the IO-shaped
Logger(the primary export offunctype-log) does NOT structurally satisfy coreLogger— its methods returnIO<never, never, void>instead ofvoid. UsetoDirectLogger(ioLogger)to bridge from one to the other when you need to mix IO-aware code with imperative consumers.
functype-* ecosystem package or any library that wants to log without picking a logging stack.bootDiagnostics for application startup with a custom logger.functype-log directly and you want full IO<never, never, void> composition with .tap/.flatMap — use functype-log’s Logger (IO-shaped) directly, not the core type.child(context) propagation or per-call context binding — those are richer-logger concerns; pick functype-log or wrap your own.