Skip to main content
ansicode
TypeScript (Deno / Bun / Node)

ANSI escape codes in TypeScript (Deno, Bun, Node)

TypeScript itself transpiles away — the runtime bytes are whatever JavaScript would emit. The interesting difference between the three TS runtimes is the stdlib write call: Node uses process.stdout.write, Deno uses Deno.stdout.writeSync (or console.log), Bun accepts both Node's process.stdout shim and its own Bun.write(Bun.stdout, …). All three are byte-clean. For typed ergonomics reach for picocolors (zero-dep, works in every runtime via npm: / jsr: / direct import), Deno's bundled @std/fmt/colors, or kolorist for a tiny isomorphic option. chalk 5+ is ESM-only and works under Node / Bun / npm:chalk in Deno.

Recommended libraries

  • picocolors

    Zero-dep, ~100 LOC, fully typed. The fastest cold-start option and the only one all three runtimes can import the same way (npm:picocolors from Deno, plain import from Node / Bun).

  • chalk

    Canonical chainable API (chalk.bold.red('oops')), full TypeScript types, supports 16 / 256 / 16M colour detection. v5+ is pure ESM — works on Node 14.16+, Bun, and Deno via npm:chalk.

  • @std/fmt/colors (Deno)

    Deno-bundled colour helpers — red(), bold(), bgBlue() — no install, no dependencies. Use this in pure-Deno scripts; for cross-runtime code, prefer picocolors / kolorist so the same file runs everywhere.

  • kolorist

    Tiny isomorphic colour helper — works in Node / Bun / Deno and browsers (falls back to CSS %c styling in DevTools). Used by Vite, marko, preact. Typed end-to-end.

Idiomatic patterns

Runtime-aware direct write (Node / Deno / Bun)
// Picks the stdout API of whichever runtime is executing. All three are
// byte-clean — \x1b sequences pass through unchanged.
const RED_BOLD = '\x1b[1;31m';
const RESET = '\x1b[0m';
const line = `${RED_BOLD}error:${RESET} permission denied\n`;

declare const Deno: { stdout: { writeSync(b: Uint8Array): number } } | undefined;
declare const Bun: { stdout: unknown; write(d: unknown, s: string): Promise<number> } | undefined;

if (typeof Deno !== 'undefined') {
  Deno.stdout.writeSync(new TextEncoder().encode(line));
} else if (typeof Bun !== 'undefined') {
  await Bun.write(Bun.stdout, line);
} else {
  process.stdout.write(line);
}
Typed SGR helper with template literal types
// Compile-time check that you only pass known SGR codes — typos fail tsc.
type SGR =
  | 0 | 1 | 2 | 3 | 4 | 7 | 22 | 23 | 24 | 27
  | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 39
  | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97;

const sgr = (text: string, ...codes: SGR[]): string =>
  `\x1b[${codes.join(';')}m${text}\x1b[0m`;

console.log(sgr('error:', 1, 31), 'permission denied');
console.log(sgr('ok', 32));
Deno: @std/fmt/colors (no install)
// deno run --allow-env script.ts
import { bold, red, rgb24 } from 'jsr:@std/fmt/colors';

console.log(`${bold(red('error:'))} permission denied`);
console.log(rgb24('lavender truecolor', 0xcba6f7));
Universal NO_COLOR detection across runtimes
// Works under Node, Bun, and Deno — each exposes env vars differently.
declare const Deno: { env: { get(k: string): string | undefined }; stdout: { isTerminal(): boolean } } | undefined;

const env = (k: string): string | undefined =>
  typeof Deno !== 'undefined' ? Deno.env.get(k) : process.env[k];

const isTTY = (): boolean =>
  typeof Deno !== 'undefined' ? Deno.stdout.isTerminal() : Boolean(process.stdout.isTTY);

const USE_COLOR = isTTY() && env('NO_COLOR') === undefined && env('FORCE_COLOR') !== '0';

export const paint = (text: string, sgr: string): string =>
  USE_COLOR ? `\x1b[${sgr}m${text}\x1b[0m` : text;

Related sequences

Other languages