Go to TypeScript transpiler
See the codeYour Go code runs anywhere TypeScript runs: Node, Bun, and the browser.
GoScript is a Go to TypeScript compiler, so Go can run anywhere TypeScript
runs. It loads packages from a Go module, type-checks them with the Go
toolchain, and emits deterministic TypeScript packages under
@goscript/<go-package>/.
GoScript compiles package graphs, generics, interfaces, pointer and value
semantics, goroutines, channels, select, defer, async call propagation,
package tests, and a practical standard-library override surface. The output
stays readable enough to inspect, bundle, and debug like code you wrote.
GoScript is developed and tuned against
Spacewave, a large Go and TypeScript app
framework. Spacewave compiles its browser core plugin through GoScript,
including its go-git storage backend and the go-mysql-server SQL engine, and
runs its core package tests through goscript test in CI. That dogfooding keeps
build speed and runtime compatibility tied to complex application code instead
of toy examples.
GoScript shares GopherJS's long-term browser goal: make ordinary Go programs run in JavaScript environments. The difference is the runtime strategy. GopherJS models a Go runtime with its own goroutine scheduler. GoScript emits readable TypeScript modules and maps concurrency onto JavaScript async work and runtime channel helpers instead of implementing a full goroutine scheduler.
Use GoScript when Go is the source of truth and part of the product must run in a TypeScript runtime. It compiles real application code: database engines, git implementations, cryptography, and concurrent framework code, not just self-contained algorithms.
Good fits today include:
GoScript does not run every valid Go program: code that depends on unsafe
memory operations, cgo, or standard-library packages without an override or
clean transpilation is unsupported. See Limitations for the
precise list.
Useful docs:
The compiler runs large real-world package graphs. Each claim below names its proof: a compliance fixture under tests/tests (500+ fixtures, each a Go program compiled, typechecked, and executed against expected output), a runtime test under gs/, or a consuming project.
go/packages with GOOS=js and GOARCH=wasm,
with build tags through CLI build flags (tests/tests/*, all fixtures)struct_*, interface_* fixtures)VarRef runtime model
(address_of_pointer_deref, gs/builtin/varRef.ts)array_*, slice_*, map_* fixtures)generic_* fixtures)select, defer, and async call propagation, mapped
onto JavaScript async/await plus the runtime scheduler
(goroutines*, channel_*, select_* fixtures; gs/builtin/scheduler.ts)goto and labeled statements through state-machine lowering
(forward_goto_statement)int64 and uint64 compile to TypeScript bigint
with Go overflow semantics (wide_uint64_exact_arithmetic,
constant_shift_64, gs/builtin/wide-int.test.ts)Math.imul (imul_32bit), float32
rounding through Math.fround (float32_rounding), and bit operations
through Math.clz32 (gs/math/bits)reflect subset covering types, values, struct fields, maps,
MakeFunc, FuncOf, and DeepEqual (reflect_* fixtures, gs/reflect/)crypto
(aes, cipher, ecdh, ed25519, rand, sha1, sha256, sha512), compress
(gzip, zlib), encoding (binary, json), os and syscall/js filesystem
support, net/http, database/sql/driver, go/token, go/scanner,
time, sync, reflect, and testinggs/github.com/, including
go-git/go-billy, klauspost/compress, zeebo/blake3, mr-tron/base58,
pkg/errors, hack-pad/safejs, and protobuf-go-litegoscript test, which compiles Go package tests to TypeScript, typechecks
the generated workspace, and runs it with Bun or in a Chromium browser
(--browser), reporting failures with compiler-stage classificationsgoscript test
(spacewave/package.json,
scripts test:go:goscript and test:go:e2e:wasm:goscript)compiler/wasm/compile_test.go, the website playground)main.go
files.goscript/wasm:imports-unsupported diagnostic
(compiler/wasm/compile_test.go). Imported code uses the package workflow.unsafe type-checks, but most operations (Alignof, Offsetof, Sizeof,
pointer conversion) throw at runtime (gs/unsafe/unsafe.ts). Pointer
arithmetic and cgo are unsupported.int, uint, uintptr, and integers narrower than 64 bits compile
to JavaScript number; only int64 and uint64 are bigint. uint and
uintptr arithmetic routes through the 64-bit runtime helpers to preserve
full width, but plain int does not model 64-bit overflow
(compiler/lowering.go, isBigIntBackedType).gs/ override must transpile cleanly or it is unsupported; there
are no real sockets, processes, or plugin loading beyond what the JavaScript
host provides.reflect override is a subset; remaining parity gaps are tracked in
gs/reflect/parity.json.goscript test supports a GoScript-compatible subset of testing, not the
complete go test flag surface (cmd/goscript/cmd-test_test.go).bigint; both
cost more than plain synchronous JavaScript with number. Benchmarks live
under tests/bench.Install Bun for TypeScript tests, examples, and website builds:
curl -fsSL https://bun.sh/install | bash
Install the CLI:
go install github.com/s4wave/goscript/cmd/goscript@latest
Compile a Go package from a module directory:
goscript compile --package . --output ./output
The output tree looks like this:
output/
└── @goscript/
├── builtin/
└── example.com/my/module/
├── index.ts
└── main.gs.ts
For a generated package main, GoScript emits a main-script guard so the module
can run directly in Bun or a bundler that resolves @goscript/* imports. See
example/simple for the smallest compile-and-run workflow.
Generated package indexes re-export generated files such as ./main.gs.ts, and
some package-local imports also use explicit .ts specifiers. Your TypeScript
project needs to allow those imports and map @goscript/* to the generated
output root.
Use this shape as the starting point:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022", "esnext.disposable", "DOM"],
"baseUrl": ".",
"paths": {
"@goscript/*": ["./output/@goscript/*"]
},
"allowImportingTsExtensions": true,
"rewriteRelativeImportExtensions": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"skipLibCheck": true,
"strict": true
}
}
The important settings are:
moduleResolution: "bundler" so @goscript/* package imports resolve like a modern app build.allowImportingTsExtensions: true because generated indexes and same-package imports can reference .ts files directly.rewriteRelativeImportExtensions: true if TypeScript is emitting JavaScript instead of only typechecking.paths pointing at the generated @goscript/ tree.If your bundler owns JavaScript emission and TypeScript only typechecks, adding
"noEmit": true is also a good fit.
goscript compile \
--package ./my-go-package \
--output ./output
Common options:
--package <pattern>: Go package pattern to compile. Repeat for multiple packages.--output <dir>: output directory for the generated TypeScript tree.--dir <dir>: working directory for module/package loading.--build-flags <flag>: Go build flag, repeatable.--all-dependencies: compile dependency packages instead of only requested packages.--gs-path <dir>: additional GoScript override root containing package-path directories.--package-blocklist <paths>: comma-separated Go import paths to reject from the compiled package graph.--compiler-cache-root <dir>: explicit compiler package artifact cache root.--protobuf-ts-binding: bind .pb.go files to sibling .pb.ts files instead of emitting .pb.gs.ts.--deferred-function: repeat for each exported, non-generic package/path.Function to load on first call. This opts into late package initialization and requires eager callers to move shared concrete types and values into a separate package. Calls become asynchronous; function values remain lazy until invoked. Configure the equivalent deferredFunctions array through the TypeScript API.--disable-emit-builtin: skip copying handwritten gs/ runtime packages.Run Go package tests through GoScript:
goscript test --tags goscript ./...
goscript test loads package test variants, compiles each selected package
through the normal GoScript pipeline, writes a TypeScript test runner, typechecks
the generated workspace, and runs it with Bun. Useful options:
--tags <tags>: comma-separated Go build tags.--run <regexp>: run only matching Go test names.--count <n>: run selected tests multiple times.--short: report true from testing.Short.--timeout <duration>: maximum package-test runtime.--workdir <dir>: generated test workspace directory.--output <dir>: generated TypeScript output root.-p <n>: maximum package typecheck/runtime commands to run concurrently.--browser: run package runtimes in a Chromium browser instead of Bun.--runtime-groups: run package runtimes in grouped Bun worker processes.--incremental-typecheck: reuse TypeScript build-info files in the test workdir.The output is shaped like go test where possible and classifies failures that
occur before the generated tests run.
Go API:
package main
import (
"context"
"github.com/s4wave/goscript/compiler"
)
func main() {
comp, err := compiler.NewCompiler(&compiler.Config{
Dir: ".",
OutputPath: "./output",
}, nil, nil)
if err != nil {
panic(err)
}
if _, err := comp.CompilePackages(context.Background(), "."); err != nil {
panic(err)
}
}
Node/Bun API:
import { compile } from 'goscript'
await compile({
pkg: '.',
output: './output',
dir: process.cwd(),
})
WASM adapter package:
package main
import "github.com/s4wave/goscript/compiler/wasm"
func main() {
ts, err := wasm.CompileSource(`
package main
func main() {
println("hello from GoScript")
}
`, "main")
if err != nil {
panic(err)
}
_ = ts
}
The website compiles this package into the browser build. Browser source compilation accepts import-free single-file demos. Package imports return a structured diagnostic; compile imported code with the package workflow.
GoScript uses a linear compiler pipeline:
public adapter
-> compile request
-> package graph
-> semantic model
-> lowered program
-> TypeScript emitter
-> runtime/override package copy
Each stage has a small, testable job:
@goscript/builtin imports stable.This separation keeps type and runtime decisions out of string rendering, so generated output changes are easier to explain, test, and debug.
Install dependencies:
bun install
Run the core checks:
bun run test
bun run lint
bun run build
Run the simple package example:
bun run example
Build the static website and browser demo assets:
bun run website:build
The website playground can compile and run import-free single-file demos in the browser. Compliance examples and imported-package examples are precompiled by the website build.
GoScript is experimental. Small compatibility shims are usually the wrong fix; prefer adding focused compiler or compliance tests that name the missing Go behavior, then implement the behavior in the compiler or runtime stage that actually owns it.
Use the repo scripts rather than direct package-manager commands:
bun run test
bun run lint
bun run build
Please open issues for unsupported Go shapes, runtime gaps, and standard-library override gaps.
MIT
1,570 commits
51 commits
41 commits
13 commits
TypeScript
84.6%
Go
12.9%
HTML
2.1%
Go to TypeScript transpiler
See the codeYour Go code runs anywhere TypeScript runs: Node, Bun, and the browser.
GoScript is a Go to TypeScript compiler, so Go can run anywhere TypeScript
runs. It loads packages from a Go module, type-checks them with the Go
toolchain, and emits deterministic TypeScript packages under
@goscript/<go-package>/.
GoScript compiles package graphs, generics, interfaces, pointer and value
semantics, goroutines, channels, select, defer, async call propagation,
package tests, and a practical standard-library override surface. The output
stays readable enough to inspect, bundle, and debug like code you wrote.
GoScript is developed and tuned against
Spacewave, a large Go and TypeScript app
framework. Spacewave compiles its browser core plugin through GoScript,
including its go-git storage backend and the go-mysql-server SQL engine, and
runs its core package tests through goscript test in CI. That dogfooding keeps
build speed and runtime compatibility tied to complex application code instead
of toy examples.
GoScript shares GopherJS's long-term browser goal: make ordinary Go programs run in JavaScript environments. The difference is the runtime strategy. GopherJS models a Go runtime with its own goroutine scheduler. GoScript emits readable TypeScript modules and maps concurrency onto JavaScript async work and runtime channel helpers instead of implementing a full goroutine scheduler.
Use GoScript when Go is the source of truth and part of the product must run in a TypeScript runtime. It compiles real application code: database engines, git implementations, cryptography, and concurrent framework code, not just self-contained algorithms.
Good fits today include:
GoScript does not run every valid Go program: code that depends on unsafe
memory operations, cgo, or standard-library packages without an override or
clean transpilation is unsupported. See Limitations for the
precise list.
Useful docs:
The compiler runs large real-world package graphs. Each claim below names its proof: a compliance fixture under tests/tests (500+ fixtures, each a Go program compiled, typechecked, and executed against expected output), a runtime test under gs/, or a consuming project.
go/packages with GOOS=js and GOARCH=wasm,
with build tags through CLI build flags (tests/tests/*, all fixtures)struct_*, interface_* fixtures)VarRef runtime model
(address_of_pointer_deref, gs/builtin/varRef.ts)array_*, slice_*, map_* fixtures)generic_* fixtures)select, defer, and async call propagation, mapped
onto JavaScript async/await plus the runtime scheduler
(goroutines*, channel_*, select_* fixtures; gs/builtin/scheduler.ts)goto and labeled statements through state-machine lowering
(forward_goto_statement)int64 and uint64 compile to TypeScript bigint
with Go overflow semantics (wide_uint64_exact_arithmetic,
constant_shift_64, gs/builtin/wide-int.test.ts)Math.imul (imul_32bit), float32
rounding through Math.fround (float32_rounding), and bit operations
through Math.clz32 (gs/math/bits)reflect subset covering types, values, struct fields, maps,
MakeFunc, FuncOf, and DeepEqual (reflect_* fixtures, gs/reflect/)crypto
(aes, cipher, ecdh, ed25519, rand, sha1, sha256, sha512), compress
(gzip, zlib), encoding (binary, json), os and syscall/js filesystem
support, net/http, database/sql/driver, go/token, go/scanner,
time, sync, reflect, and testinggs/github.com/, including
go-git/go-billy, klauspost/compress, zeebo/blake3, mr-tron/base58,
pkg/errors, hack-pad/safejs, and protobuf-go-litegoscript test, which compiles Go package tests to TypeScript, typechecks
the generated workspace, and runs it with Bun or in a Chromium browser
(--browser), reporting failures with compiler-stage classificationsgoscript test
(spacewave/package.json,
scripts test:go:goscript and test:go:e2e:wasm:goscript)compiler/wasm/compile_test.go, the website playground)main.go
files.goscript/wasm:imports-unsupported diagnostic
(compiler/wasm/compile_test.go). Imported code uses the package workflow.unsafe type-checks, but most operations (Alignof, Offsetof, Sizeof,
pointer conversion) throw at runtime (gs/unsafe/unsafe.ts). Pointer
arithmetic and cgo are unsupported.int, uint, uintptr, and integers narrower than 64 bits compile
to JavaScript number; only int64 and uint64 are bigint. uint and
uintptr arithmetic routes through the 64-bit runtime helpers to preserve
full width, but plain int does not model 64-bit overflow
(compiler/lowering.go, isBigIntBackedType).gs/ override must transpile cleanly or it is unsupported; there
are no real sockets, processes, or plugin loading beyond what the JavaScript
host provides.reflect override is a subset; remaining parity gaps are tracked in
gs/reflect/parity.json.goscript test supports a GoScript-compatible subset of testing, not the
complete go test flag surface (cmd/goscript/cmd-test_test.go).bigint; both
cost more than plain synchronous JavaScript with number. Benchmarks live
under tests/bench.Install Bun for TypeScript tests, examples, and website builds:
curl -fsSL https://bun.sh/install | bash
Install the CLI:
go install github.com/s4wave/goscript/cmd/goscript@latest
Compile a Go package from a module directory:
goscript compile --package . --output ./output
The output tree looks like this:
output/
└── @goscript/
├── builtin/
└── example.com/my/module/
├── index.ts
└── main.gs.ts
For a generated package main, GoScript emits a main-script guard so the module
can run directly in Bun or a bundler that resolves @goscript/* imports. See
example/simple for the smallest compile-and-run workflow.
Generated package indexes re-export generated files such as ./main.gs.ts, and
some package-local imports also use explicit .ts specifiers. Your TypeScript
project needs to allow those imports and map @goscript/* to the generated
output root.
Use this shape as the starting point:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022", "esnext.disposable", "DOM"],
"baseUrl": ".",
"paths": {
"@goscript/*": ["./output/@goscript/*"]
},
"allowImportingTsExtensions": true,
"rewriteRelativeImportExtensions": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"skipLibCheck": true,
"strict": true
}
}
The important settings are:
moduleResolution: "bundler" so @goscript/* package imports resolve like a modern app build.allowImportingTsExtensions: true because generated indexes and same-package imports can reference .ts files directly.rewriteRelativeImportExtensions: true if TypeScript is emitting JavaScript instead of only typechecking.paths pointing at the generated @goscript/ tree.If your bundler owns JavaScript emission and TypeScript only typechecks, adding
"noEmit": true is also a good fit.
goscript compile \
--package ./my-go-package \
--output ./output
Common options:
--package <pattern>: Go package pattern to compile. Repeat for multiple packages.--output <dir>: output directory for the generated TypeScript tree.--dir <dir>: working directory for module/package loading.--build-flags <flag>: Go build flag, repeatable.--all-dependencies: compile dependency packages instead of only requested packages.--gs-path <dir>: additional GoScript override root containing package-path directories.--package-blocklist <paths>: comma-separated Go import paths to reject from the compiled package graph.--compiler-cache-root <dir>: explicit compiler package artifact cache root.--protobuf-ts-binding: bind .pb.go files to sibling .pb.ts files instead of emitting .pb.gs.ts.--deferred-function: repeat for each exported, non-generic package/path.Function to load on first call. This opts into late package initialization and requires eager callers to move shared concrete types and values into a separate package. Calls become asynchronous; function values remain lazy until invoked. Configure the equivalent deferredFunctions array through the TypeScript API.--disable-emit-builtin: skip copying handwritten gs/ runtime packages.Run Go package tests through GoScript:
goscript test --tags goscript ./...
goscript test loads package test variants, compiles each selected package
through the normal GoScript pipeline, writes a TypeScript test runner, typechecks
the generated workspace, and runs it with Bun. Useful options:
--tags <tags>: comma-separated Go build tags.--run <regexp>: run only matching Go test names.--count <n>: run selected tests multiple times.--short: report true from testing.Short.--timeout <duration>: maximum package-test runtime.--workdir <dir>: generated test workspace directory.--output <dir>: generated TypeScript output root.-p <n>: maximum package typecheck/runtime commands to run concurrently.--browser: run package runtimes in a Chromium browser instead of Bun.--runtime-groups: run package runtimes in grouped Bun worker processes.--incremental-typecheck: reuse TypeScript build-info files in the test workdir.The output is shaped like go test where possible and classifies failures that
occur before the generated tests run.
Go API:
package main
import (
"context"
"github.com/s4wave/goscript/compiler"
)
func main() {
comp, err := compiler.NewCompiler(&compiler.Config{
Dir: ".",
OutputPath: "./output",
}, nil, nil)
if err != nil {
panic(err)
}
if _, err := comp.CompilePackages(context.Background(), "."); err != nil {
panic(err)
}
}
Node/Bun API:
import { compile } from 'goscript'
await compile({
pkg: '.',
output: './output',
dir: process.cwd(),
})
WASM adapter package:
package main
import "github.com/s4wave/goscript/compiler/wasm"
func main() {
ts, err := wasm.CompileSource(`
package main
func main() {
println("hello from GoScript")
}
`, "main")
if err != nil {
panic(err)
}
_ = ts
}
The website compiles this package into the browser build. Browser source compilation accepts import-free single-file demos. Package imports return a structured diagnostic; compile imported code with the package workflow.
GoScript uses a linear compiler pipeline:
public adapter
-> compile request
-> package graph
-> semantic model
-> lowered program
-> TypeScript emitter
-> runtime/override package copy
Each stage has a small, testable job:
@goscript/builtin imports stable.This separation keeps type and runtime decisions out of string rendering, so generated output changes are easier to explain, test, and debug.
Install dependencies:
bun install
Run the core checks:
bun run test
bun run lint
bun run build
Run the simple package example:
bun run example
Build the static website and browser demo assets:
bun run website:build
The website playground can compile and run import-free single-file demos in the browser. Compliance examples and imported-package examples are precompiled by the website build.
GoScript is experimental. Small compatibility shims are usually the wrong fix; prefer adding focused compiler or compliance tests that name the missing Go behavior, then implement the behavior in the compiler or runtime stage that actually owns it.
Use the repo scripts rather than direct package-manager commands:
bun run test
bun run lint
bun run build
Please open issues for unsupported Go shapes, runtime gaps, and standard-library override gaps.
MIT
1,570 commits
51 commits
41 commits
13 commits
TypeScript
84.6%
Go
12.9%
HTML
2.1%