Skip to content
typesafe-docsPublic

About

Go's fmt.Sprintf for JavaScript and TypeScript: every verb and flag, exact float formatting, Go's %!verb(type=value) error forms, and explicit int/float/uint wrappers.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

go-printf

Go's fmt.Sprintf for JavaScript and TypeScript, matching Go's output byte for byte on scalar values.

import { sprintf, float64, int64, uint8 } from 'go-printf';

sprintf('%06d|%+.2f|%-8s|%x', 42, 3.14159, 'go', 255); // '000042|+3.14|go      |ff'
sprintf('%v %v', 1e6, float64(1e6));                    // '1000000 1e+06'
sprintf('%d %d', uint8(300), int64(2n ** 63n));         // '44 -9223372036854775808'
sprintf('%q %+q %#q', 'tab\there', '日本', 'raw');       // '"tab\there" "日本" `raw`'
sprintf('%d %s', 'x', 5);                               // '%!d(string=x) %!s(int=5)'

Why

JavaScript number formatters implement C printf or PHP sprintf, not Go's. They differ on %v, %q, %x of strings, %c, %U, flag handling, and the %!verb(type=value) error forms. Ports of Go's text/template or fmt need the real behavior, and each one hand-rolls a partial version.

Measured: npm run bench:incumbents. The corpus is 7,087 scalar cases (int, float, string, bool; verbs v d s q x X o b c U e E f g G t; flags - + # 0 and space; widths and precisions) whose expected output comes from fmt.Sprintf in Go 1.27.1, regenerated by scripts/corpus. The incumbents are called the way their READMEs show, with the value as the argument.

gofmt@0.1.0                             5132/7087  72.4%
sprintf-js@1.1.3                        2495/7087  35.2%
printj@1.3.1                            2545/7087  35.9%
fast-printf@1.6.10                       649/7087  9.2%
go-printf (bare JS values)              6385/7087  90.1%
go-printf (int()/float64() wrappers)    7087/7087  100.0%

Also measured with the same corpus in the research pass (not in this script, it needs a bundler alias): the gs/fmt package of goscript 0.3.3, a Go-to-TypeScript transpiler, scored 3801/7087 (53.6%). locutus and @tannin/sprintf scored 0.

The bare-value row loses cases where the corpus passes a Go float64 that happens to be integral (100), which JavaScript cannot distinguish from an int. That is the reason for the wrappers below.

Installation

npm install go-printf

Zero runtime dependencies. Node 20.6 or later.

Usage

Choosing the Go type

A JavaScript number does not say whether it is an int or a float, or how wide. Bare values map as follows.

JS value Go type Note
integer-valued number, abs < 2**63 int except -0
any other number (fraction, NaN, Infinity, -0, 1e21) float64
bigint int64 throws RangeError outside int64
string string
boolean bool
null, undefined nil %v prints <nil>
Uint8Array []byte works with %s %x %X %q %v %d
Array of the above slice see below

Wrap a value when the default is wrong.

import { sprintf, float64, float32, int64, uint64, uint8, rune, gostring } from 'go-printf';

sprintf('%v', 100);                   // '100'   int
sprintf('%e', float64(100));          // '1.000000e+02'
sprintf('%v', float32(0.1));          // '0.1'   Go's 32-bit shortest form
sprintf('%x', uint64(-1n));           // 'ffffffffffffffff'
sprintf('%c %q', rune(0x1f600), rune(0x1f600));

Strings that are not valid UTF-8

sprintf returns a JS string, so bytes that Go would emit as invalid UTF-8 come back as U+FFFD. sprintfBytes returns the exact bytes. gostring(bytes) passes a Go string that is not valid UTF-8 (a Uint8Array is a []byte, which prints differently under %v).

import { sprintfBytes, gostring } from 'go-printf';

sprintf('%q', gostring(new Uint8Array([0x61, 0xff]))); // '"a\xff"'
sprintfBytes('%s', gostring(new Uint8Array([0xff])));  // Uint8Array [ 255 ]

Arrays

An array prints as Go prints a slice: [a b c], with the verb, flags, width and precision applied to each element. Element types come from the same mapping as bare values. This is verified against Go for slices of int, int32, string, float64, bool and for []byte. %T and %#v infer the element type from the array ([]int, or []interface {} when mixed or empty).

sprintf('%v %q %5.1f', [1, 2, 3], ['a', 'b c'], [1.25, 2.5]); // '[1 2 3] ["a" "b c"] [  1.2   2.5]'

Reference

sprintf(format, ...args): string

Go's fmt.Sprintf. Throws TypeError for arguments it cannot represent (objects, Map, functions, symbols) and RangeError for a bigint outside int64. It never throws for a bad format; those produce Go's error text.

sprintfBytes(format: string | Uint8Array, ...args): Uint8Array

Same, returning the output bytes. format may be raw bytes.

Verbs, flags, width, precision

Element Supported
Verbs %v %d %s %q %x %X %o %O %b %c %U %e %E %f %F %g %G %t %p %T %% (%w is a bad verb, as in Sprintf)
Flags + - # space 0, including %+v and %#v
Width, precision digits, *, %[n]*d, %.*f, %.[n]*f
Argument index %[2]d, %[1]*d
Error forms %!d(string=x), %!(EXTRA type=value, ...), %!d(MISSING), %!d(BADINDEX), %!(NOVERB), %!(BADWIDTH), %!(BADPREC), %!z(int=1)
Float verbs %b and %x/%X (hex floats) as well as the decimal ones

%p has no meaning in JavaScript. On a non-slice it prints the bad-verb form, as Go does for non-pointers. On an array or Uint8Array it prints a fixed placeholder address (0xc000000000) that carries no information.

Type wrappers

Function Go type Function Go type
int(n) int (64-bit) uint(n) uint
int8 int16 int32 same uint8 uint16 uint32 same
int64(n) int64 uint64(n) uint64
rune(n) rune (int32) byte(n) byte (uint8)
uintptr(n) uintptr float64(n) float64
float32(n) float32 gostring(bytes) string from raw bytes

Integer wrappers take a number (must be an integer, else TypeError) or a bigint, and wrap around to the type's width as a Go conversion does: uint8(300) is 44. float32(n) rounds with Math.fround.

formatFloat(v, fmt, prec, bits): string

strconv.FormatFloat for fmt in b e E f g G x X, with bits 32 or 64. prec -1 is the shortest representation.

How it works

The formatter is a close port of Go's fmt/print.go and fmt/format.go, so flag interactions (%-05d, %+08.3f, % x, %#U) follow the same code paths. Strings are processed as UTF-8 bytes, so width counts runes and invalid bytes the way Go does.

Floats use strconv rules. Shortest output (%v, %g with no precision) uses JavaScript's own shortest digits for 64-bit values (the same digits as Go) and a round-trip search for 32-bit values. Every other conversion expands the exact binary value into decimal digits with BigInt and rounds half to even, so %.30f, %.400f and %.2f of 1e21 are exact where toFixed and toExponential are not.

%q follows strconv.Quote, including the printable-rune table taken from Go's strconv/isprint.go (Go's Unicode version, not the Node/ICU one), \x, \u and \U escapes, and \xNN for each invalid UTF-8 byte.

Parts of Go's source were ported closely. They keep the "Copyright The Go Authors" headers, and Go's BSD-3 license text is shipped in LICENSE-go.

Limitations

  • Composite values are out of scope. Arrays of scalars print as [a b c]; objects, Map, Set, structs, pointers and complex numbers are not supported, and %v of an object throws TypeError.
  • Arrays are verified only for slices of int, int32, string, float64, bool (and Uint8Array) with the verbs in test/fixtures. Nested arrays, mixed arrays and %T/%#v of arrays are best effort.
  • Formatter, Stringer, GoStringer and error methods are not consulted.
  • %p is meaningless (see Reference).
  • int is 64-bit, as on 64-bit Go platforms.
  • sprintf replaces invalid UTF-8 in its output with U+FFFD; use sprintfBytes. A lone surrogate in a JS string format or argument is encoded as U+FFFD, since Go strings cannot hold one.
  • The reference is Go 1.27.1. Go has changed fmt details between releases, so output can differ from older toolchains.
  • Speed was not tuned. Integer formatting goes through BigInt.

Compatibility

Tested: Node 24.20.0 (TypeScript suite, built dist/ suite), Go 1.27.1 darwin/arm64 as the oracle. CI is configured for Node 20, 22 and 24; those runs are not part of the measurements here. Untested: Node 20 and 22, Windows, browsers (the code uses only TextEncoder, TextDecoder, BigInt and DataView).

Development

npm install
npx tsc --noEmit
npm test                     # unit tests + recorded Go output (no Go needed)
npm run build && npm run test:dist
npm run test:live            # 30000 fresh random cases against the local `go`
npm run bench:corpus         # research corpus, regenerated from Go
npm run bench:incumbents     # the table above (installs incumbents into .cache/)
node bench/fuzz.ts --seconds 60   # crash fuzz, differential fuzz, all code points
npm run fixtures             # regenerate test/fixtures with the local Go

scripts/oracle is the Go program behind the differential tests. oracle gen produces random formats (verbs, flags, width, precision, *, [n] indexes, junk verbs, missing and extra arguments) with typed arguments and records Go's output. oracle run evaluates given cases. test/fixtures/golden.ndjson (hand-written cases) and test/fixtures/random.ndjson.gz (5,700 random cases) hold recorded Go output.

License

MIT. See LICENSE. Portions derived from the Go standard library are under the BSD 3-clause license in LICENSE-go.

About

Go's fmt.Sprintf for JavaScript and TypeScript: every verb and flag, exact float formatting, Go's %!verb(type=value) error forms, and explicit int/float/uint wrappers.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages