brainstorming
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or planning anything new, in software or out of it (a talk, a business, a renovation).
lean-ts-patternsPatterns for building lightweight, zero-dependency TypeScript tools and libraries. Use when building new CLI tools, libraries, or utilities from scratch. Use when refactoring existing TypeScript projects to remove unnecessary dependencies. Use when writing fetch wrappers, CLI parsers, loggers, config merging, string utilities, or any infrastructure code tha…
Independent GitHub snapshot · Automated classification · Original documentation governs
Patterns for building lightweight, zero-dependency TypeScript/Bun tools. Distilled from studying exemplary repos: unjs/citty (CLI), unjs/consola (logging), unjs/ofetch (HTTP), unjs/defu (merging), unjs/scule (strings), unjs/pathe (paths), antfu/taze (package updates).
Inline tiny utils. Use node: builtins. Vendor at build time if needed.
node:util.parseArgs (not commander/yargs)path polyfills)globalThis.fetch wrapper (not axios)defineCommand, defineConfig return their argument unchanged. Their only job is type inference:
function defineCommand<const T extends ArgsDef>(def: CommandDef<T>): CommandDef<T> {
return def;
}
The const modifier preserves literal types. Without it, { type: "boolean" } widens to { type: string }.
Every library has one core function. Everything else is a thin wrapper:
splitByCase() -> camelCase, kebabCase, pascalCase, snakeCase, trainCasenormalizeWindowsPath() -> join, resolve, normalize, relative, etc._defu() -> defu, defuFn, defuArrayFn_logFn() -> .info(), .error(), .warn(), .debug(), etc.Closures that capture config and return composable instances:
function createFetch(globalOpts = {}) {
const $fetch = async (url, opts) => { /* ... */ };
$fetch.create = (defaults) => createFetch({ ...globalOpts, defaults });
return $fetch;
}
Used by: ofetch (createFetch), consola (createConsola), defu (createDefu), citty (createMain).
One type that enables lazy loading everywhere:
type Resolvable<T> = T | Promise<T> | (() => T) | (() => Promise<T>);
function resolveValue<T>(input: Resolvable<T>): T | Promise<T> {
return typeof input === "function" ? (input as any)() : input;
}
Use for subcommands, config, metadata -- anything that might be expensive to compute upfront.
null/undefined = "not set", let defaults fill inIf the runtime dispatches on a discriminant, the type system should too:
// Runtime: switch on type
if (arg.type === "boolean") { /* ... */ }
// Types: conditional on same discriminant
type ParsedArg<T> =
T["type"] extends "boolean" ? boolean :
T["type"] extends "string" ? string :
T["type"] extends "enum" ? T["options"][number] :
never;
const noColor = (() => {
const env = globalThis.process?.env ?? {};
return env.NO_COLOR === "1" || env.TERM === "dumb" || env.CI;
})();
type ColorFn = (t: string) => string;
const _c = (c: number, r = 39): ColorFn => (t) =>
noColor ? t : `\u001b[${c}m${t}\u001b[${r}m`;
export const bold = _c(1, 22);
export const dim = _c(2, 22);
export const red = _c(31);
export const green = _c(32);
export const yellow = _c(33);
export const blue = _c(34);
export const cyan = _c(36);
export const gray = _c(90);
function isPlainObject(value: unknown): value is Record<string, unknown> {
if (value === null || typeof value !== "object") return false;
const proto = Object.getPrototypeOf(value);
if (proto !== null && proto !== Object.prototype
&& Object.getPrototypeOf(proto) !== null) return false;
if (Symbol.iterator in value) return false;
if (Symbol.toStringTag in value)
return Object.prototype.toString.call(value) === "[object Module]";
return true;
}
type MaybeArray<T> = T | T[];
type MaybePromise<T> = T | Promise<T>;
async function callHooks<C>(
context: C,
hooks: MaybeArray<(ctx: C) => MaybePromise<void>> | undefined,
): Promise<void> {
if (!hooks) return;
for (const hook of Array.isArray(hooks) ? hooks : [hooks]) {
await hook(context);
}
}
const DRIVE_RE = /^[A-Za-z]:\//;
function normalizeWindowsPath(input = "") {
if (!input) return input;
return input
.replace(/\\/g, "/")
.replace(DRIVE_RE, (r) => r.toUpperCase());
}
| Need | Pattern | Reference |
|---|---|---|
| CLI argument parsing | node:util.parseArgs + typed layer | cli-patterns.md |
| Colored terminal output | ANSI helper above | Inline above |
| HTTP client with retries | Fetch factory + interceptors | fetch-patterns.md |
| Logger with levels/reporters | Single-method reporter interface | logging-patterns.md |
| Deep object merging | Defaults-first recursive merge | data-utils.md |
| String case conversion | splitByCase + join variants | data-utils.md |
| Type-safe definitions | const generic + conditional types | typescript-tricks.md |
| Lazy loading | Resolvable<T> + dynamic import | Inline above |
| Cross-platform paths | normalizeWindowsPath at every entry | data-utils.md |
node:util.parseArgs covers 95% of CLI needs_ prefix convention