Convert TypeScript code to idiomatic Go. Use when migrating TypeScript projects to Go, translating TypeScript patterns to idiomatic Go, or refactoring TypeScript codebases into Go. Extends meta-convert-dev with TypeScript-to-Go specific patterns.
Convert TypeScript code to idiomatic Go. Use when migrating TypeScript projects to Go, translating TypeScript patterns to idiomatic Go, or refactoring TypeScript codebases into Go. Extends meta-convert-dev with TypeScript-to-Go specific patterns.
Convert TypeScript to Go
Convert TypeScript code to idiomatic Go. This skill extends meta-convert-dev with TypeScript-to-Go specific type mappings, idiom translations, and tooling.
For pointers, interfaces, slices, maps, channels, functions
undefined
Zero value
Each type has a zero value (0, "", false, nil)
symbol
No direct equivalent
Use string or int constants
any
interface{} or any
any alias added in Go 1.18
unknown
interface{} with type assertion
Requires type checking
void
(no return)
Function returns nothing
never
No direct equivalent
Functions that never return use panic
Collection Types
TypeScript
Go
Notes
T[]
[]T
Slice (resizable, passed by reference)
Array<T>
[]T
Same as T[]
readonly T[]
[]T
Go doesn't enforce readonly at compile time
[number, number, number]
[3]T
Fixed-size array
[T, U]
struct { A T; B U }
Named struct preferred
[T, U, V]
struct { A T; B U; C V }
Named struct preferred
Map<K, V>
map[K]V
Hash map
Record<K, V>
map[K]V
Hash map
Set<T>
map[T]struct{}
Empty struct uses zero memory
Set<T>
map[T]bool
Alternative using bool (1 byte per entry)
WeakMap
No direct equivalent
Use map with manual cleanup
WeakSet
No direct equivalent
Use map with manual cleanup
Composite Types
TypeScript
Go
Notes
interface X { ... } (data)
type X struct { ... }
Data structures
interface X { method(): T }
type X interface { Method() T }
Behavior contracts
class X
type X struct + methods
Struct with receiver methods
type X = Y
type X = Y
Type alias (Go 1.9+)
type X = Y | Z
Custom type with methods
Discriminated union pattern
T | null
*T
Pointer can be nil
T | undefined
*T or zero value check
Pointer or explicit check
Partial<T>
Struct with pointer fields
Each field can be nil
Required<T>
Struct with value fields
All fields have values
Pick<T, K>
New struct type
Select fields manually
Omit<T, K>
New struct type
Exclude fields manually
enum X
const block with iota
Or typed constants
namespace X
package X
Package organization
Generic Type Mappings
TypeScript
Go
Notes
<T>
[T any]
Generic type parameter (Go 1.18+)
<T extends U>
[T U]
Type constraint using interface
<T extends keyof U>
No direct equivalent
Use reflection or code generation
Array<T>
[]T
Built-in generic slice
Promise<T>
chan T or function return
Channels for async communication
Readonly<T>
No language support
Convention and documentation
Record<K, V>
map[K]V
Built-in generic map
Idiom Translation
Pattern: Null/Undefined Handling
TypeScript:
const name = user?.name ?? "Anonymous";
const age = user?.age || 18;
Go:
var name string
if user != nil && user.Name != "" {
name = user.Name
} else {
name = "Anonymous"
}
age := 18
if user != nil && user.Age > 0 {
age = user.Age
}
Why this translation:
Go doesn't have optional chaining or null coalescing operators
Explicit nil checks are idiomatic and clear
Zero values (0, "", false) should be considered in logic
arr1 := []int{1, 2, 3}
arr2 := append(append([]int{}, arr1...), 4, 5)
// Or clearer:
arr2 := make([]int, len(arr1), len(arr1)+2)
copy(arr2, arr1)
arr2 = append(arr2, 4, 5)
// No built-in object spread; must copy manually
obj2 := struct{ A, B, C int }{
A: obj1.A,
B: obj1.B,
C: 3,
}
Why this translation:
Go uses append with ... for variadic slice expansion
Pre-allocating capacity avoids reallocation
No object spread; manual field copying required
Reflection can help for generic copying but adds complexity
Pattern: Optional Properties
TypeScript:
interface User {
name: string;
email?: string;
age?: number;
}
Go:
type User struct {
Name string
Email *string // pointer indicates optional
Age *int // nil means not provided
}
// Helper to create pointer
func StringPtr(s string) *string { return &s }
func IntPtr(i int) *int { return &i }
// Usage
user := User{
Name: "Alice",
Email: StringPtr("alice@example.com"),
}
Why this translation:
Pointers distinguish between "not provided" (nil) and "zero value"
Helper functions make pointer creation cleaner
Alternative: use zero values and a separate "set" map
Consider whether nil vs zero value distinction is needed
Pattern: Union Types
TypeScript:
type Result = { success: true; data: string } | { success: false; error: string };
function process(): Result {
if (Math.random() > 0.5) {
return { success: true, data: "OK" };
}
return { success: false, error: "Failed" };
}
Go:
type Result struct {
Success bool
Data string // only valid if Success == true
Error string // only valid if Success == false
}
func Process() Result {
if rand.Float64() > 0.5 {
return Result{Success: true, Data: "OK"}
}
return Result{Success: false, Error: "Failed"}
}
// Or use interface with type assertion
type ResultSuccess struct{ Data string }
type ResultError struct{ Error string }
func Process() interface{} {
if rand.Float64() > 0.5 {
return ResultSuccess{Data: "OK"}
}
return ResultError{Error: "Failed"}
}
Why this translation:
Go doesn't have union types
Use struct with discriminator field (Success bool)
Interface{} with type assertion for true sum types
Consider if error return pattern is more idiomatic
Pattern: String Interpolation
TypeScript:
const name = "Alice";
const age = 30;
const message = `Hello, ${name}! You are ${age} years old.`;
Go:
name := "Alice"
age := 30
message := fmt.Sprintf("Hello, %s! You are %d years old.", name, age)
Why this translation:
Go uses fmt.Sprintf for string formatting
Printf-style format verbs (%s, %d, %v, etc.)
Type-safe at runtime, not compile-time
Alternative: strings.Builder for complex concatenation
Error Handling
TypeScript Exception Model → Go Error Return Model
TypeScript:
function parseConfig(path: string): Config {
if (!fs.existsSync(path)) {
throw new Error(`Config file not found: ${path}`);
}
const content = fs.readFileSync(path, 'utf-8');
try {
return JSON.parse(content);
} catch (e) {
throw new Error(`Failed to parse config: ${e.message}`);
}
}
// Usage
try {
const config = parseConfig("config.json");
console.log(config);
} catch (err) {
console.error("Error:", err.message);
}
class ValidationError extends Error {
constructor(public field: string, message: string) {
super(message);
this.name = "ValidationError";
}
}
class NotFoundError extends Error {
constructor(public resource: string) {
super(`${resource} not found`);
this.name = "NotFoundError";
}
}
function validateUser(user: User): void {
if (!user.email) {
throw new ValidationError("email", "Email is required");
}
}
Go:
// Custom error types implement error interface
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error on field %s: %s", e.Field, e.Message)
}
type NotFoundError struct {
Resource string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s not found", e.Resource)
}
func ValidateUser(user *User) error {
if user.Email == "" {
return &ValidationError{Field: "email", Message: "Email is required"}
}
return nil
}
// Usage with type assertion
err := ValidateUser(user)
if err != nil {
var validationErr *ValidationError
if errors.As(err, &validationErr) {
log.Printf("Validation failed on field: %s", validationErr.Field)
}
}
Why this translation:
Go uses error interface (Error() string method)
Custom error types are structs with Error() method
errors.As for type-safe error checking
errors.Is for sentinel error comparison
Wrap errors with fmt.Errorf("%w", err) to preserve chain
Panic vs Error Returns
TypeScript:
// Exceptions for everything
function divide(a: number, b: number): number {
if (b === 0) {
throw new Error("division by zero");
}
return a / b;
}
Go:
// Error returns for expected errors
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// Panic only for programmer errors (bugs)
func DivideMustNotBeZero(a, b float64) float64 {
if b == 0 {
panic("division by zero - caller error")
}
return a / b
}
Why this translation:
Go distinguishes expected errors (return) from bugs (panic)
Use error returns for conditions caller should handle
Use panic for programmer errors / assertions
recover() can catch panics (similar to catch) but rarely used
// ✓ Pass by value for small types
func Double(n int) int {
return n * 2
}
Why:
Small types (int, bool, small structs) are efficient by value
Pointers add indirection and nil checks
Use pointers for: large structs, mutation, or optional values
3. Modifying Loop Variables in Goroutines
Problem:
// ❌ Loop variable capture bug
for _, item := range items {
go func() {
process(item) // All goroutines see last item!
}()
}
Solution:
// ✓ Pass variable as parameter or shadow it
for _, item := range items {
item := item // shadow
go func() {
process(item)
}()
}
// Or pass as parameter
for _, item := range items {
go func(i string) {
process(i)
}(item)
}
Why:
Loop variable is reused across iterations
Goroutines capture variable reference, not value
Fixed in Go 1.22+ with per-iteration variables
4. Not Closing Channels
Problem:
// ❌ Channel never closed
ch := make(chan int)
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
// Never closes!
}()
for val := range ch {
fmt.Println(val) // Hangs after 10 values
}
Solution:
// ✓ Close channel when done
ch := make(chan int)
go func() {
defer close(ch)
for i := 0; i < 10; i++ {
ch <- i
}
}()
for val := range ch {
fmt.Println(val)
}
Why:
range on channel blocks until closed
close() signals no more values coming
Only sender should close (not receiver)
5. Misunderstanding Zero Values
Problem:
// TypeScript: undefined check
if (user.age !== undefined) {
// age was explicitly set
}
// ❌ Go: can't distinguish zero value from explicit zero
if user.Age != 0 {
// Could be unset OR explicitly set to 0!
}
Solution:
// ✓ Use pointers for optional values
type User struct {
Name string
Age *int // nil means not set, 0 means explicitly zero
}
if user.Age != nil {
fmt.Println(*user.Age)
}
Why:
Go initializes all variables to zero values
Can't distinguish "not set" from "set to zero"
Use pointers when distinction matters
6. Forgetting defer for Cleanup
Problem:
// ❌ Manual cleanup easy to forget
file, err := os.Open("file.txt")
if err != nil {
return err
}
// ... lots of code ...
if someError {
return someError // Forgot to close file!
}
file.Close()
Solution:
// ✓ defer ensures cleanup
file, err := os.Open("file.txt")
if err != nil {
return err
}
defer file.Close() // Always runs before function returns
// ... code can return anywhere ...
Why:
defer guarantees cleanup on all return paths
Executes in LIFO order
Common for: files, mutexes, database connections
7. Copying Mutexes
Problem:
// ❌ Copying struct with mutex
type Counter struct {
mu sync.Mutex
count int
}
func (c Counter) Inc() { // Value receiver copies mutex!
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
Solution:
// ✓ Pointer receiver for structs with mutexes
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
Why:
Copying a locked mutex is undefined behavior
sync types (Mutex, WaitGroup, etc.) must not be copied
Use pointer receivers for types with sync primitives
8. Interface nil Confusion
Problem:
// ❌ Interface containing nil pointer isn't nil!
var p *User = nil
var i interface{} = p
if i != nil {
fmt.Println("Not nil!") // This prints!
}
Solution:
// ✓ Check for nil before assigning to interface
var p *User = nil
if p != nil {
i = p
} else {
i = nil // Or don't assign
}
// Or use typed nil check
var i interface{} = (*User)(nil)
if i == nil || i.(*User) == nil {
// Actually nil
}
package main
import "fmt"
type User struct {
ID string
Name string
Age int
Email *string // pointer for optional field
}
func FindUserByID(users []User, id string) *User {
for i := range users {
if users[i].ID == id {
return &users[i]
}
}
return nil
}
func main() {
email := "alice@example.com"
users := []User{
{ID: "1", Name: "Alice", Age: 30, Email: &email},
{ID: "2", Name: "Bob", Age: 25, Email: nil},
}
user := FindUserByID(users, "1")
if user != nil {
fmt.Printf("Found: %s\n", user.Name)
}
}
Example 2: Medium - Error Handling and JSON
Before (TypeScript):
import * as fs from 'fs';
interface Config {
host: string;
port: number;
debug: boolean;
}
class ConfigError extends Error {
constructor(message: string) {
super(message);
this.name = "ConfigError";
}
}
function loadConfig(path: string): Config {
if (!fs.existsSync(path)) {
throw new ConfigError(`Config file not found: ${path}`);
}
const content = fs.readFileSync(path, 'utf-8');
try {
const config = JSON.parse(content);
if (!config.host || typeof config.port !== 'number') {
throw new ConfigError("Invalid config format");
}
return config as Config;
} catch (e) {
if (e instanceof ConfigError) {
throw e;
}
throw new ConfigError(`Failed to parse config: ${(e as Error).message}`);
}
}
// Usage
try {
const config = loadConfig("config.json");
console.log(`Server running on ${config.host}:${config.port}`);
} catch (err) {
if (err instanceof ConfigError) {
console.error("Config error:", err.message);
process.exit(1);
}
}
After (Go):
package main
import (
"encoding/json"
"fmt"
"os"
)
type Config struct {
Host string `json:"host"`
Port int `json:"port"`
Debug bool `json:"debug"`
}
type ConfigError struct {
Message string
}
func (e *ConfigError) Error() string {
return e.Message
}
func LoadConfig(path string) (*Config, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, &ConfigError{
Message: fmt.Sprintf("config file not found: %s", path),
}
}
content, err := os.ReadFile(path)
if err != nil {
return nil, &ConfigError{
Message: fmt.Sprintf("failed to read config: %v", err),
}
}
var config Config
if err := json.Unmarshal(content, &config); err != nil {
return nil, &ConfigError{
Message: fmt.Sprintf("failed to parse config: %v", err),
}
}
if config.Host == "" || config.Port == 0 {
return nil, &ConfigError{
Message: "invalid config format",
}
}
return &config, nil
}
func main() {
config, err := LoadConfig("config.json")
if err != nil {
var configErr *ConfigError
if errors.As(err, &configErr) {
fmt.Fprintf(os.Stderr, "Config error: %s\n", configErr.Message)
os.Exit(1)
}
}
fmt.Printf("Server running on %s:%d\n", config.Host, config.Port)
}
Example 3: Complex - HTTP API with Async Operations