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)'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.
npm install go-printfZero runtime dependencies. Node 20.6 or later.
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));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 ]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]'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.
Same, returning the output bytes. format may be raw bytes.
| 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.
| 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.
strconv.FormatFloat for fmt in b e E f g G x X, with bits 32 or 64. prec -1 is the shortest representation.
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.
- 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%vof an object throwsTypeError. - Arrays are verified only for slices of
int,int32,string,float64,bool(andUint8Array) with the verbs intest/fixtures. Nested arrays, mixed arrays and%T/%#vof arrays are best effort. Formatter,Stringer,GoStringeranderrormethods are not consulted.%pis meaningless (see Reference).intis 64-bit, as on 64-bit Go platforms.sprintfreplaces invalid UTF-8 in its output with U+FFFD; usesprintfBytes. 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
fmtdetails between releases, so output can differ from older toolchains. - Speed was not tuned. Integer formatting goes through
BigInt.
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).
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 Goscripts/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.
MIT. See LICENSE. Portions derived from the Go standard library are under the BSD 3-clause license in LICENSE-go.