Output & Running
Hello, World
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
} console.log("Hello, World!"); TypeScript needs no
package main, no import, and no func main — top-level statements run in source order. console.log is the everyday equivalent of fmt.Println and adds a trailing newline the same way. The semicolon is optional in TypeScript, but most style guides keep it.Compiling & running
// Run a program in one step:
// go run main.go
//
// Build a standalone binary:
// go build -o app main.go && ./app // Type-check, transpile, and run in one step (tsx / ts-node):
// npx tsx main.ts
//
// Compile to plain JavaScript first, then run with Node:
// npx tsc main.ts && node main.js Unlike Go, TypeScript does not produce a self-contained native binary — it transpiles to JavaScript that a runtime such as Node.js executes.
tsx compiles and runs in one step like go run, while tsc emits .js files like go build emits a binary. The type checker is a separate step from execution, and a program with type errors still runs.Formatted output
package main
import "fmt"
func main() {
name := "TypeScript"
version := 6.0
fmt.Printf("%s %.1f\n", name, version)
} const name = "TypeScript";
const version = 6.0;
console.log(`${name} ${version.toFixed(1)}`); TypeScript has no
Printf verbs. Template literals use backticks with ${...} to interpolate any expression — the closest analogue to building a string before printing it. Numeric formatting is done with methods such as toFixed(1) rather than a %.1f verb.Comments
package main
import "fmt"
func main() {
// Single-line comment
/* Block comment */
count := 0
fmt.Println(count)
} // Single-line comment
/* Block comment */
/**
* JSDoc comment — tools and editors read this for
* documentation and type hints.
*/
const count: number = 0;
console.log(count); Comment syntax is identical to Go:
// and /* ... */. TypeScript adds the /** ... */ JSDoc convention, which editors surface as hover documentation and which can even supply types in plain JavaScript files — there is no Go equivalent that the toolchain reads at edit time.Variables & Types
Declaring variables
package main
import "fmt"
func main() {
var explicit int = 42
inferred := "hello" // short declaration
fmt.Println(explicit, inferred)
} const explicit: number = 42;
const inferred = "hello"; // type inferred as string
console.log(explicit, inferred); TypeScript writes the type after the name with a colon (
explicit: number), the reverse of Go's var explicit int. Just like Go's :=, an initializer lets the type be inferred. Prefer const (single assignment) over let (reassignable); var exists but is function-scoped and discouraged.Constants & enums
package main
import "fmt"
type Direction int
const (
North Direction = iota
East
South
West
)
func main() {
fmt.Println(North, West)
} enum Direction {
North,
East,
South,
West,
}
console.log(Direction.North, Direction.West); TypeScript's
enum auto-numbers its members from 0, exactly like Go's iota pattern, but it is a real named type with reverse mapping (Direction[0] === "North"). For plain compile-time constants, TypeScript uses const; unlike Go there is no block form, so each constant is its own const statement.Basic types
package main
import "fmt"
func main() {
var count int = 10
var ratio float64 = 3.14
var active bool = true
var label string = "go"
fmt.Println(count, ratio, active, label)
} const count: number = 10;
const ratio: number = 3.14;
const active: boolean = true;
const label: string = "ts";
console.log(count, ratio, active, label); TypeScript collapses Go's whole integer and floating-point family —
int, int64, float64, and the rest — into a single number (an IEEE-754 double). There is no separate integer type and no overflow wrapping; bool becomes boolean and string keeps its name. For 64-bit integers beyond 2^53, TypeScript offers a distinct bigint type.Zero values vs null & undefined
package main
import "fmt"
func main() {
var count int // 0
var label string // ""
var active bool // false
fmt.Printf("%d %q %t\n", count, label, active)
} // With strictNullChecks, a variable must be assigned before use.
let count: number = 0;
let label: string = "";
let active: boolean = false;
// A value that may be absent is declared explicitly:
let nickname: string | null = null;
console.log(count, JSON.stringify(label), active, nickname); Go gives every variable a zero value (
0, "", false, nil) automatically. TypeScript has no zero values: under strictNullChecks you must initialize a variable, and the absence of a value is its own concern — undefined (never assigned) and null (deliberately empty) are distinct types you opt into with a union like string | null. This is what eliminates Go's "forgot to check for nil" class of bug at compile time.Type conversion
package main
import (
"fmt"
"strconv"
)
func main() {
var whole int = 65
var asFloat float64 = float64(whole)
var asText string = strconv.Itoa(whole)
fmt.Println(asFloat, asText)
} const whole: number = 65;
const asFloat: number = whole; // int and float are both "number"
const asText: string = String(whole); // or whole.toString()
console.log(asFloat, asText); Go requires an explicit conversion between every numeric type (
float64(whole)); TypeScript has only one numeric type, so int-to-float conversions simply do not exist. Converting a number to a string uses String(value) or value.toString() rather than strconv.Itoa. Note these are runtime conversions — the TypeScript as keyword is a compile-time type assertion that does nothing at runtime.Strings
String length & indexing
package main
import "fmt"
func main() {
greeting := "héllo"
fmt.Println(len(greeting)) // 6 bytes (é is 2 bytes)
fmt.Println(greeting[0]) // 104, a byte
} const greeting = "héllo";
console.log(greeting.length); // 5 — UTF-16 code units
console.log(greeting[0]); // "h", a one-character string
console.log(greeting.charCodeAt(0)); // 104 Go strings are immutable byte slices:
len counts bytes and indexing yields a byte. TypeScript strings are sequences of UTF-16 code units: .length counts units and indexing returns a single-character string, not a numeric code. Neither counts user-perceived characters for astral-plane glyphs, but the everyday failure modes differ — bytes in Go, surrogate pairs in TypeScript.String interpolation
package main
import "fmt"
func main() {
name := "World"
count := 3
message := fmt.Sprintf("Hello, %s! You have %d messages.", name, count)
fmt.Println(message)
} const name = "World";
const count = 3;
const message = `Hello, ${name}! You have ${count} messages.`;
console.log(message); Where Go builds a string with
fmt.Sprintf and % verbs, TypeScript embeds expressions directly inside a backtick template literal with ${...}. Any expression is allowed, not just variables, and no format verb or type matching is needed — values are converted to strings automatically.Common string methods
package main
import (
"fmt"
"strings"
)
func main() {
phrase := " Go and TypeScript "
fmt.Println(strings.TrimSpace(phrase))
fmt.Println(strings.ToUpper(phrase))
fmt.Println(strings.Contains(phrase, "Type"))
fmt.Println(strings.Replace(phrase, "and", "vs", 1))
} const phrase = " Go and TypeScript ";
console.log(phrase.trim());
console.log(phrase.toUpperCase());
console.log(phrase.includes("Type"));
console.log(phrase.replace("and", "vs")); Go keeps string operations in the
strings package as free functions that take the string as an argument; TypeScript hangs them off the string value as methods (phrase.trim()). Note replace swaps only the first match by default — pass a regular expression with the g flag to replace all, the equivalent of Go's strings.ReplaceAll.Split & join
package main
import (
"fmt"
"strings"
)
func main() {
csv := "ruby,go,typescript"
parts := strings.Split(csv, ",")
fmt.Println(parts) // [ruby go typescript]
fmt.Println(strings.Join(parts, " | "))
} const csv = "ruby,go,typescript";
const parts: string[] = csv.split(",");
console.log(parts); // [ 'ruby', 'go', 'typescript' ]
console.log(parts.join(" | ")); The operations mirror Go almost exactly, but the spelling moves onto the value:
csv.split(",") instead of strings.Split(csv, ","), and parts.join(" | ") instead of strings.Join(parts, " | "). The result of split is a typed string[] array.Runes & code points
package main
import "fmt"
func main() {
word := "café"
for index, runeValue := range word {
fmt.Printf("%d:%c ", index, runeValue)
}
fmt.Println()
} const word = "café";
for (const character of word) {
process.stdout.write(character + " ");
}
console.log(); Ranging over a Go string decodes UTF-8 into runes and gives you a byte offset plus the rune. TypeScript's
for...of over a string iterates by Unicode code point — so é arrives as one item even though it spans two UTF-16 units — which is closer to Go's rune iteration than indexing by [i] would be. There is no built-in byte offset; use .entries() if you need an index.Numbers
One number type
package main
import "fmt"
func main() {
var small int8 = 127
var big int64 = 9000000000
var precise float64 = 0.1 + 0.2
fmt.Println(small, big, precise)
} const small: number = 127;
const big: number = 9000000000;
const precise: number = 0.1 + 0.2; // 0.30000000000000004
console.log(small, big, precise); Go gives you a sized integer for every need (
int8, uint32, int64, …) and distinguishes integers from floats. TypeScript has a single number, a 64-bit float, so integers are exact only up to 2^53 and there is no overflow wrapping — a value simply loses precision. The same IEEE-754 rounding that makes 0.1 + 0.2 imprecise in Go's float64 applies to every TypeScript number.Integer vs float division
package main
import "fmt"
func main() {
fmt.Println(7 / 2) // 3 — integer division
fmt.Println(7.0 / 2.0) // 3.5
fmt.Println(7 % 2) // 1
} console.log(7 / 2); // 3.5 — always floating point
console.log(Math.trunc(7 / 2)); // 3 — truncate toward zero
console.log(7 % 2); // 1
console.log(Math.floor(-7 / 2)); // -4 This is a classic Go-to-TypeScript trap:
7 / 2 is 3 in Go because both operands are integers, but 3.5 in TypeScript because every number is a float. To get Go's integer division you must truncate explicitly with Math.trunc (toward zero) or Math.floor (toward negative infinity). The % operator works on floats too, unlike Go where it is integer-only.Math operations
package main
import (
"fmt"
"math"
)
func main() {
fmt.Println(math.Sqrt(144))
fmt.Println(math.Max(3, 9))
fmt.Println(math.Pow(2, 10))
fmt.Println(math.Round(2.7))
} console.log(Math.sqrt(144));
console.log(Math.max(3, 9));
console.log(Math.pow(2, 10)); // or 2 ** 10
console.log(Math.round(2.7)); Go's
math package and TypeScript's built-in Math object cover the same ground with lowercase-first method names instead of Go's exported capitals. TypeScript also has an exponentiation operator, 2 ** 10, with no Go equivalent. Math.max accepts any number of arguments, where Go's math.Max takes exactly two.Parsing numbers
package main
import (
"fmt"
"strconv"
)
func main() {
whole, err := strconv.Atoi("42")
if err != nil {
fmt.Println("bad number")
return
}
ratio, _ := strconv.ParseFloat("3.14", 64)
fmt.Println(whole, ratio)
} const whole = Number.parseInt("42", 10);
if (Number.isNaN(whole)) {
console.log("bad number");
} else {
const ratio = Number.parseFloat("3.14");
console.log(whole, ratio);
} Go signals a parse failure with a second
error return that you must check. TypeScript instead returns the special value NaN, which you test for with Number.isNaN — there is no error to ignore, but also nothing forcing you to check. Always pass the radix 10 to parseInt to avoid surprises with strings like "0x10".Arrays & Slices
Slice & array literals
package main
import "fmt"
func main() {
numbers := []int{1, 2, 3}
names := []string{"go", "ts"}
fmt.Println(numbers, len(numbers))
fmt.Println(names)
} const numbers: number[] = [1, 2, 3];
const names: Array<string> = ["go", "ts"];
console.log(numbers, numbers.length);
console.log(names); A TypeScript array is the everyday equivalent of a Go slice: growable, reference-like, and written
number[] (or the generic form Array<string>). There is no separate fixed-length array type as in Go's [3]int; for that, TypeScript uses a tuple type, [number, number, number]. Length is a property, numbers.length, not a function.Appending elements
package main
import "fmt"
func main() {
queue := []string{"first"}
queue = append(queue, "second", "third")
fmt.Println(queue)
fmt.Println(len(queue))
} const queue: string[] = ["first"];
queue.push("second", "third");
console.log(queue);
console.log(queue.length); Go's
append returns a new (possibly reallocated) slice that you must reassign — a common source of bugs when the return value is dropped. TypeScript's push mutates the array in place and returns the new length, so even a const array can grow: const freezes the binding, not the contents. To copy rather than mutate, spread into a new array: [...queue, "fourth"].Iterating
package main
import "fmt"
func main() {
colors := []string{"red", "green", "blue"}
for index, color := range colors {
fmt.Printf("%d: %s\n", index, color)
}
} const colors = ["red", "green", "blue"];
colors.forEach((color, index) => {
console.log(`${index}: ${color}`);
});
// or a for...of loop when you need break/continue:
for (const [index, color] of colors.entries()) {
console.log(`${index}: ${color}`);
} Go's
for index, value := range gives you both the index and the element. TypeScript offers two idioms: forEach((value, index) => ...) — note the argument order is reversed from Go — or a for...of loop over colors.entries() when you need break or continue, which a callback cannot provide.Slicing
package main
import "fmt"
func main() {
digits := []int{0, 1, 2, 3, 4, 5}
fmt.Println(digits[1:4]) // [1 2 3]
fmt.Println(digits[:2]) // [0 1]
fmt.Println(digits[3:]) // [3 4 5]
} const digits = [0, 1, 2, 3, 4, 5];
console.log(digits.slice(1, 4)); // [ 1, 2, 3 ]
console.log(digits.slice(0, 2)); // [ 0, 1 ]
console.log(digits.slice(3)); // [ 3, 4, 5 ] TypeScript's
slice(start, end) matches Go's digits[start:end] — end-exclusive, with an optional end. The crucial difference: Go's slice expression shares the underlying array, so writes through one slice can be seen by another, whereas Array.slice always returns a shallow copy. There is no aliasing surprise in TypeScript.Map, filter, reduce
package main
import "fmt"
func main() {
numbers := []int{1, 2, 3, 4, 5}
doubled := []int{}
sum := 0
for _, value := range numbers {
if value%2 == 1 {
doubled = append(doubled, value*2)
}
sum += value
}
fmt.Println(doubled, sum)
} const numbers = [1, 2, 3, 4, 5];
const doubled = numbers
.filter((value) => value % 2 === 1)
.map((value) => value * 2);
const sum = numbers.reduce((total, value) => total + value, 0);
console.log(doubled, sum); Go has no
map/filter/reduce in its standard library philosophy — you write the loop by hand. TypeScript inherits JavaScript's chainable array methods, so transformations read as a pipeline. Each returns a new array (or value) without mutating the original, and the element type flows through so the compiler knows doubled is a number[].Sorting
package main
import (
"fmt"
"sort"
)
func main() {
scores := []int{30, 10, 20}
sort.Slice(scores, func(left, right int) bool {
return scores[left] < scores[right]
})
fmt.Println(scores)
} const scores = [30, 10, 20];
scores.sort((left, right) => left - right);
console.log(scores); Go's
sort.Slice takes a less function returning a bool. TypeScript's Array.sort wants a comparator returning a number: negative, zero, or positive. For numbers, left - right ascends. Beware: sort with no comparator sorts as strings (so [10, 9] becomes [10, 9]), and it mutates the array in place like Go.Objects & Maps
Maps
package main
import "fmt"
func main() {
ages := map[string]int{
"alice": 30,
"bob": 25,
}
ages["carol"] = 35
fmt.Println(ages["alice"], len(ages))
} const ages = new Map<string, number>([
["alice", 30],
["bob", 25],
]);
ages.set("carol", 35);
console.log(ages.get("alice"), ages.size); TypeScript's
Map is the direct counterpart to Go's map: any key type, insertion-ordered, sized by .size. You read and write with .get/.set rather than bracket indexing. A plain object ({ alice: 30 }) also works as a string-keyed dictionary, but Map preserves insertion order, allows non-string keys, and will not collide with prototype properties.Checking for a key
package main
import "fmt"
func main() {
inventory := map[string]int{"apples": 3}
count, ok := inventory["apples"]
if ok {
fmt.Println("have", count)
}
if _, found := inventory["pears"]; !found {
fmt.Println("no pears")
}
} const inventory = new Map<string, number>([["apples", 3]]);
if (inventory.has("apples")) {
console.log("have", inventory.get("apples"));
}
if (!inventory.has("pears")) {
console.log("no pears");
} Go's comma-ok idiom (
value, ok := m[key]) distinguishes "key absent" from "key present with zero value". TypeScript's Map.has(key) answers presence directly, and .get returns undefined for a missing key — which, under strictNullChecks, the compiler forces you to handle. There is no silent zero value masquerading as a real entry.Iterating a map
package main
import (
"fmt"
"sort"
)
func main() {
prices := map[string]int{"pen": 2, "book": 9}
keys := make([]string, 0, len(prices))
for key := range prices {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
fmt.Printf("%s=%d\n", key, prices[key])
}
} const prices = new Map<string, number>([
["pen", 2],
["book", 9],
]);
for (const [item, price] of prices) {
console.log(`${item}=${price}`);
} Go deliberately randomizes map iteration order, so reproducible output means collecting and sorting the keys first. A TypeScript
Map iterates in insertion order, deterministically, and a for...of loop destructures each [key, value] entry directly — no separate key slice needed. This predictability is one of Map's advantages over Go's intentional shuffle.Sets
package main
import "fmt"
func main() {
seen := map[string]struct{}{}
seen["a"] = struct{}{}
seen["b"] = struct{}{}
seen["a"] = struct{}{}
_, has := seen["a"]
fmt.Println(len(seen), has)
} const seen = new Set<string>();
seen.add("a");
seen.add("b");
seen.add("a"); // duplicate ignored
console.log(seen.size, seen.has("a")); Go has no set type, so the idiom is a
map[T]struct{} where the empty struct uses no memory. TypeScript ships a real Set: add, has, delete, size, and iteration in insertion order. It is the natural choice for de-duplication, and new Set(array) followed by [...set] is the standard way to unique an array.Control Flow
If / else if
package main
import "fmt"
func main() {
score := 75
if score >= 90 {
fmt.Println("A")
} else if score >= 70 {
fmt.Println("B")
} else {
fmt.Println("C")
}
} const score = 75;
if (score >= 90) {
console.log("A");
} else if (score >= 70) {
console.log("B");
} else {
console.log("C");
} The structure is the same, with one syntactic flip: TypeScript requires parentheses around the condition, where Go forbids them. Beware truthiness — TypeScript treats
0, "", null, undefined, and NaN as falsy, so if (score) is not the same as if (score !== 0). Go has no such coercion: conditions must be genuine booleans.Switch
package main
import "fmt"
func main() {
day := "sat"
switch day {
case "sat", "sun":
fmt.Println("weekend")
default:
fmt.Println("weekday")
}
} const day = "sat";
switch (day) {
case "sat":
case "sun":
console.log("weekend");
break;
default:
console.log("weekday");
} Go cases do not fall through and can list several values in one
case. TypeScript inherits C-style fall-through, so each case needs an explicit break, and matching multiple values means stacking empty case labels. Forgetting a break is a real bug class in TypeScript that simply cannot occur in Go.Loops
package main
import "fmt"
func main() {
for i := 0; i < 3; i++ {
fmt.Println("count", i)
}
n := 0
for n < 2 { // Go's "while"
n++
}
fmt.Println(n)
} for (let index = 0; index < 3; index++) {
console.log("count", index);
}
let n = 0;
while (n < 2) { // TypeScript has a real while keyword
n++;
}
console.log(n); Go has exactly one loop keyword,
for, that doubles as a while loop and an infinite loop. TypeScript keeps the C family's separate while and do...while alongside the three-clause for. Use let for the loop variable so each iteration's binding is block-scoped, which matters when you capture it in a closure.Ternary & defaults
package main
import "fmt"
func main() {
score := 55
// Go has no ternary; use if/else.
label := "fail"
if score >= 60 {
label = "pass"
}
fmt.Println(label)
} const score = 55;
const label = score >= 60 ? "pass" : "fail";
console.log(label);
// Nullish coalescing supplies a default for null/undefined:
const nickname: string | undefined = undefined;
console.log(nickname ?? "anonymous"); Go deliberately omits the ternary operator, forcing an
if/else. TypeScript keeps condition ? a : b, which makes conditional initialization a one-liner. It also adds the nullish-coalescing operator ?? that supplies a fallback only when the left side is null or undefined (not merely falsy) — handy for default values where Go would check nil explicitly.Functions
Defining functions
package main
import "fmt"
func add(left int, right int) int {
return left + right
}
func main() {
fmt.Println(add(3, 4))
} function add(left: number, right: number): number {
return left + right;
}
// Arrow-function form, common for callbacks:
const subtract = (left: number, right: number): number => left - right;
console.log(add(3, 4), subtract(9, 5)); A TypeScript function declares parameter types after each name and the return type after the parameter list, mirroring Go's
func add(left int) int with colons. Return types are usually inferred, so they are optional. The arrow form (const subtract = (...) => ...) is a concise expression-bodied function with no Go equivalent, ubiquitous for callbacks.Multiple return values
package main
import "fmt"
func divmod(dividend, divisor int) (int, int) {
return dividend / divisor, dividend % divisor
}
func main() {
quotient, remainder := divmod(17, 5)
fmt.Println(quotient, remainder)
} function divmod(dividend: number, divisor: number): [number, number] {
return [Math.trunc(dividend / divisor), dividend % divisor];
}
const [quotient, remainder] = divmod(17, 5);
console.log(quotient, remainder); Go returns multiple values natively. TypeScript has no such thing, so the idiom is to return a tuple type
[number, number] and destructure it at the call site with const [a, b] = .... For more than two or three values, returning a named object ({ quotient, remainder }) reads better and survives reordering.Variadic parameters
package main
import "fmt"
func total(numbers ...int) int {
sum := 0
for _, value := range numbers {
sum += value
}
return sum
}
func main() {
fmt.Println(total(1, 2, 3, 4))
} function total(...numbers: number[]): number {
return numbers.reduce((sum, value) => sum + value, 0);
}
console.log(total(1, 2, 3, 4)); Both languages collect trailing arguments into a slice/array. Go writes
numbers ...int and the parameter has type []int; TypeScript writes ...numbers: number[]. To spread an existing array into the call, Go uses total(values...) and TypeScript uses total(...values) — the dots simply move to the front.Default & optional parameters
package main
import "fmt"
// Go has no default arguments; callers pass every value,
// or you accept a struct of options.
func greet(name string, greeting string) string {
if greeting == "" {
greeting = "Hello"
}
return greeting + ", " + name
}
func main() {
fmt.Println(greet("Ada", ""))
fmt.Println(greet("Ada", "Hi"))
} function greet(name: string, greeting: string = "Hello"): string {
return `${greeting}, ${name}`;
}
// A trailing parameter can also be optional with "?":
function label(text: string, suffix?: string): string {
return suffix ? `${text} (${suffix})` : text;
}
console.log(greet("Ada"));
console.log(greet("Ada", "Hi"));
console.log(label("draft")); Go has no default arguments — the common workaround is a sentinel zero value or an options struct. TypeScript supports real defaults (
greeting: string = "Hello") and optional parameters marked with ?, whose type becomes string | undefined. Optional parameters must come after required ones, just like default-valued ones.Closures
Closures
package main
import "fmt"
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
next := counter()
fmt.Println(next(), next(), next())
} function counter(): () => number {
let count = 0;
return () => {
count += 1;
return count;
};
}
const next = counter();
console.log(next(), next(), next()); Closures capture variables by reference in both languages, and the captured state outlives the enclosing call. The function-type syntax differs: Go writes
func() int, TypeScript writes () => number. The arrow function also captures this lexically, which matters inside classes — a frequent source of bugs that the arrow form quietly fixes.Functions as arguments
package main
import "fmt"
func apply(values []int, transform func(int) int) []int {
result := make([]int, len(values))
for index, value := range values {
result[index] = transform(value)
}
return result
}
func main() {
squared := apply([]int{1, 2, 3}, func(n int) int { return n * n })
fmt.Println(squared)
} function apply(values: number[], transform: (value: number) => number): number[] {
return values.map(transform);
}
const squared = apply([1, 2, 3], (n) => n * n);
console.log(squared); Passing functions as first-class values works the same way; the parameter type is just spelled differently — Go's
func(int) int versus TypeScript's (value: number) => number. Because the callback's parameter type is known from apply's signature, TypeScript infers n as a number at the call site, so you rarely annotate inline arrow functions.Structs & Classes
Structs vs classes
package main
import "fmt"
type Point struct {
X int
Y int
}
func main() {
origin := Point{X: 0, Y: 0}
moved := Point{X: 3, Y: 4}
fmt.Println(origin, moved.X)
} class Point {
constructor(
public x: number,
public y: number,
) {}
}
const origin = new Point(0, 0);
const moved = new Point(3, 4);
console.log(origin, moved.x); Go's
struct is a plain value type; TypeScript's class bundles fields and methods together and is instantiated with new. The public x: number parameter-property shorthand declares and assigns the field in one stroke — much terser than Go's separate field list. For pure data with no behavior, a TypeScript interface or type is closer to a Go struct than a class is (see the Interfaces section).Methods
package main
import "fmt"
type Rectangle struct {
Width int
Height int
}
func (r Rectangle) Area() int {
return r.Width * r.Height
}
func main() {
box := Rectangle{Width: 3, Height: 4}
fmt.Println(box.Area())
} class Rectangle {
constructor(
public width: number,
public height: number,
) {}
area(): number {
return this.width * this.height;
}
}
const box = new Rectangle(3, 4);
console.log(box.area()); Go attaches methods to a type with a receiver outside the struct (
func (r Rectangle) Area()). TypeScript declares methods inside the class body and accesses fields through this rather than a named receiver. There is no value-vs-pointer-receiver distinction: objects are always handled by reference, so a method can freely mutate the instance.Embedding vs inheritance
package main
import "fmt"
type Animal struct {
Name string
}
func (a Animal) Describe() string {
return a.Name + " is an animal"
}
type Dog struct {
Animal // embedded
Breed string
}
func main() {
rex := Dog{Animal: Animal{Name: "Rex"}, Breed: "Lab"}
fmt.Println(rex.Describe(), rex.Breed)
} class Animal {
constructor(public name: string) {}
describe(): string {
return `${this.name} is an animal`;
}
}
class Dog extends Animal {
constructor(name: string, public breed: string) {
super(name);
}
}
const rex = new Dog("Rex", "Lab");
console.log(rex.describe(), rex.breed); Go has no inheritance; it composes behavior by embedding a struct, which promotes the inner type's fields and methods. TypeScript has true single inheritance with
extends and super, so Dog genuinely is an Animal and can override describe. The Go embedding model is closer to "has-a with forwarding"; the TypeScript model is classical "is-a".Visibility & access
package main
import "fmt"
type Account struct {
Owner string // exported (capitalized)
balance int // unexported (lowercase)
}
func (a *Account) Deposit(amount int) {
a.balance += amount
}
func (a Account) Balance() int {
return a.balance
}
func main() {
account := &Account{Owner: "Ada"}
account.Deposit(100)
fmt.Println(account.Owner, account.Balance())
} class Account {
private balance = 0;
constructor(public owner: string) {}
deposit(amount: number): void {
this.balance += amount;
}
get currentBalance(): number {
return this.balance;
}
}
const account = new Account("Ada");
account.deposit(100);
console.log(account.owner, account.currentBalance); Go controls visibility by capitalization at the package boundary — exported names start uppercase. TypeScript uses explicit
public, private, and protected modifiers enforced at compile time within the class. (For true runtime privacy, JavaScript's #field syntax is also available.) The get keyword defines a computed property accessed without parentheses, which Go has no equivalent for.Interfaces & Structural Typing
Interfaces
package main
import "fmt"
type Shape interface {
Area() float64
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return 3.14159 * c.Radius * c.Radius
}
func describe(s Shape) {
fmt.Printf("area is %.2f\n", s.Area())
}
func main() {
describe(Circle{Radius: 2})
} interface Shape {
area(): number;
}
class Circle implements Shape {
constructor(public radius: number) {}
area(): number {
return Math.PI * this.radius * this.radius;
}
}
function describe(shape: Shape): void {
console.log(`area is ${shape.area().toFixed(2)}`);
}
describe(new Circle(2)); This is the most reassuring parallel for a Go developer: TypeScript interfaces are structural, exactly like Go's. A type satisfies
Shape by having the right methods, not by declaring a relationship. The implements Shape clause is optional documentation that makes the compiler check the match early; remove it and Circle still works as a Shape wherever its shape fits.Interfaces describe data, too
package main
import "fmt"
type User struct {
Name string
Age int
}
func greet(user User) string {
return "Hi " + user.Name
}
func main() {
fmt.Println(greet(User{Name: "Bo", Age: 40}))
} interface User {
name: string;
age: number;
}
function greet(user: User): string {
return `Hi ${user.name}`;
}
// No class needed — a plain object that matches the shape works:
console.log(greet({ name: "Bo", age: 40 })); Go interfaces describe method sets only; data is described by structs. TypeScript interfaces describe the shape of any value — including plain-data fields — so a bare object literal
{ name, age } satisfies User with no class or constructor. This is the everyday way to type function arguments and return values; reach for a class only when you need methods or instance state.The empty interface: any & unknown
package main
import "fmt"
func printAny(value any) {
switch typed := value.(type) {
case int:
fmt.Println("int", typed)
case string:
fmt.Println("string", typed)
default:
fmt.Println("other")
}
}
func main() {
printAny(42)
printAny("hello")
} function printAny(value: unknown): void {
if (typeof value === "number") {
console.log("number", value);
} else if (typeof value === "string") {
console.log("string", value);
} else {
console.log("other");
}
}
printAny(42);
printAny("hello"); Go's
any (an alias for the empty interface) holds any value and is unpacked with a type switch. TypeScript has two counterparts: any turns type-checking off for that value (escape hatch — avoid it), while unknown is the safe version that forces you to narrow with typeof before use, much as Go forces a type assertion. Prefer unknown; it is the honest equivalent of Go's any.Type assertions & guards
package main
import "fmt"
func main() {
var value any = "hello"
text, ok := value.(string)
if ok {
fmt.Println("string of length", len(text))
}
number, ok := value.(int)
fmt.Println(number, ok) // 0 false
} let value: unknown = "hello";
if (typeof value === "string") {
// value is narrowed to string in this block
console.log("string of length", value.length);
}
// "as" is a compile-time assertion with no runtime check:
const forced = value as string;
console.log(forced.toUpperCase()); Go's comma-ok assertion
value.(string) checks the dynamic type at runtime and reports success. TypeScript's typeof/instanceof guards do the same and narrow the variable's static type inside the block. Beware the as keyword: it is a pure compile-time claim with no runtime check — if you are wrong, the error surfaces later, unlike Go's failed assertion which panics or returns false immediately.Unions & Type Narrowing
Union types
package main
import "fmt"
// Go has no union types. The closest is an interface
// plus a type switch, or a tagged struct.
func format(value any) string {
switch typed := value.(type) {
case int:
return fmt.Sprintf("number: %d", typed)
case string:
return "text: " + typed
default:
return "unknown"
}
}
func main() {
fmt.Println(format(7))
fmt.Println(format("hi"))
} type Id = number | string;
function format(value: Id): string {
if (typeof value === "number") {
return `number: ${value}`;
}
return `text: ${value}`;
}
console.log(format(7));
console.log(format("hi")); Go cannot say "this value is an
int or a string" except through the untyped any. TypeScript's union type number | string expresses exactly that, and the compiler then requires you to handle every member before using member-specific operations. After the typeof check returns, TypeScript knows the remaining type is string with no further test — narrowing the union as control flow proceeds.Literal types
package main
import "fmt"
type Status string
const (
Active Status = "active"
Inactive Status = "inactive"
)
func setStatus(status Status) {
fmt.Println("status:", status)
}
func main() {
setStatus(Active)
// setStatus("banned") would still compile — any string works.
} type Status = "active" | "inactive";
function setStatus(status: Status): void {
console.log("status:", status);
}
setStatus("active");
// setStatus("banned"); // compile error: not assignable to Status Go's named string type
Status documents intent but does not restrict the set of values — any string literal still satisfies it. TypeScript's literal union "active" | "inactive" makes the exact allowed values part of the type, so a typo is a compile error. This is one of the genuinely new powers TypeScript offers a Go developer: types built from specific values, not just shapes.Discriminated unions
package main
import "fmt"
type Shape interface {
isShape()
}
type Circle struct{ Radius float64 }
type Square struct{ Side float64 }
func (Circle) isShape() {}
func (Square) isShape() {}
func area(shape Shape) float64 {
switch typed := shape.(type) {
case Circle:
return 3.14159 * typed.Radius * typed.Radius
case Square:
return typed.Side * typed.Side
default:
return 0
}
}
func main() {
fmt.Println(area(Circle{Radius: 2}))
fmt.Println(area(Square{Side: 3}))
} interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
side: number;
}
type Shape = Circle | Square;
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius * shape.radius;
case "square":
return shape.side * shape.side;
}
}
console.log(area({ kind: "circle", radius: 2 }));
console.log(area({ kind: "square", side: 3 })); Go models a closed set of variants with a sealed interface and a type switch. TypeScript's idiom is a discriminated union: each member carries a literal
kind tag, and switching on that tag narrows shape to the exact variant in each branch — shape.radius is only accessible after case "circle". Crucially, the compiler checks exhaustiveness: add a third shape and forget a case, and it flags the gap, something Go's type switch cannot guarantee.Generics
Generic functions
package main
import "fmt"
func first[T any](items []T) T {
return items[0]
}
func main() {
fmt.Println(first([]int{10, 20, 30}))
fmt.Println(first([]string{"a", "b"}))
} function first<T>(items: T[]): T {
return items[0];
}
console.log(first([10, 20, 30]));
console.log(first(["a", "b"])); Generics arrived in Go 1.18 with square brackets (
[T any]); TypeScript uses angle brackets (<T>) and has had them from the start. In both, the type parameter is inferred from the argument, so callers rarely write it. TypeScript's any constraint is simply the absence of a constraint — an unconstrained <T> accepts anything.Constraints
package main
import "fmt"
type Number interface {
~int | ~float64
}
func sum[T Number](values []T) T {
var total T
for _, value := range values {
total += value
}
return total
}
func main() {
fmt.Println(sum([]int{1, 2, 3}))
fmt.Println(sum([]float64{1.5, 2.5}))
} interface HasLength {
length: number;
}
function longest<T extends HasLength>(left: T, right: T): T {
return left.length >= right.length ? left : right;
}
console.log(longest("hello", "hi")); // "hello"
console.log(longest([1, 2, 3], [9])); // [1, 2, 3] Go constrains a type parameter with an interface that lists permitted underlying types (
~int | ~float64). TypeScript constrains with T extends SomeShape, meaning "any type assignable to this shape". Note the philosophical difference: Go's numeric constraints exist because operators like + are not methods, whereas TypeScript constraints are about structure — here, anything with a length property, be it a string or an array.Generic types
package main
import "fmt"
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(value T) {
s.items = append(s.items, value)
}
func (s *Stack[T]) Pop() T {
last := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return last
}
func main() {
stack := &Stack[int]{}
stack.Push(1)
stack.Push(2)
fmt.Println(stack.Pop(), stack.Pop())
} class Stack<T> {
private items: T[] = [];
push(value: T): void {
this.items.push(value);
}
pop(): T | undefined {
return this.items.pop();
}
}
const stack = new Stack<number>();
stack.push(1);
stack.push(2);
console.log(stack.pop(), stack.pop()); Generic structs and generic classes line up closely:
Stack[T] versus Stack<T>, and the type parameter threads through the methods. One safety difference stands out — TypeScript's Array.pop returns T | undefined because the stack might be empty, and strictNullChecks makes you confront that, whereas Go's slice indexing would panic at runtime on an empty stack.Error Handling
Errors vs exceptions
package main
import (
"errors"
"fmt"
)
func divide(dividend, divisor int) (int, error) {
if divisor == 0 {
return 0, errors.New("division by zero")
}
return dividend / divisor, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(result)
} function divide(dividend: number, divisor: number): number {
if (divisor === 0) {
throw new Error("division by zero");
}
return Math.trunc(dividend / divisor);
}
try {
const result = divide(10, 0);
console.log(result);
} catch (error) {
console.log("error:", (error as Error).message);
} This is the deepest cultural gap between the two languages. Go treats errors as ordinary return values you must inspect; TypeScript (like most languages) throws exceptions that unwind the stack until a
try/catch catches them. There is no (value, error) tuple and nothing forcing a caller to handle failure — an uncaught throw crashes the program. The compiler also cannot tell you which functions might throw, unlike Go's explicit error return.Custom error types
package main
import "fmt"
type ValidationError struct {
Field string
}
func (e *ValidationError) Error() string {
return "invalid field: " + e.Field
}
func validate(name string) error {
if name == "" {
return &ValidationError{Field: "name"}
}
return nil
}
func main() {
err := validate("")
if err != nil {
fmt.Println(err)
}
} class ValidationError extends Error {
constructor(public field: string) {
super(`invalid field: ${field}`);
this.name = "ValidationError";
}
}
function validate(name: string): void {
if (name === "") {
throw new ValidationError("name");
}
}
try {
validate("");
} catch (error) {
if (error instanceof ValidationError) {
console.log(error.message, "->", error.field);
}
} A Go custom error is any type implementing the
Error() string method. A TypeScript custom error extends the built-in Error class, carrying extra fields and a name. In the catch block the caught value is typed unknown (or any), so you narrow it with instanceof — the TypeScript equivalent of Go's errors.As for recovering a specific error type.Wrapping & cleanup
package main
import (
"errors"
"fmt"
)
func load() error {
base := errors.New("file not found")
return fmt.Errorf("loading config: %w", base)
}
func main() {
err := load()
fmt.Println(err)
fmt.Println(errors.Unwrap(err))
} function load(): never {
const base = new Error("file not found");
throw new Error("loading config", { cause: base });
}
try {
load();
} catch (error) {
const wrapped = error as Error;
console.log(wrapped.message);
console.log((wrapped.cause as Error).message);
} finally {
console.log("cleanup runs no matter what");
} Go wraps errors with
%w and unwraps with errors.Unwrap. Modern TypeScript mirrors this with the { cause } option on Error, readable back via error.cause. For cleanup, Go uses defer; TypeScript uses a finally block that runs whether or not an exception was thrown — the same guarantee, attached to the try rather than scheduled per call.Panic vs uncaught throw
package main
import "fmt"
func risky() {
defer func() {
if recovered := recover(); recovered != nil {
fmt.Println("recovered:", recovered)
}
}()
panic("something broke")
}
func main() {
risky()
fmt.Println("still running")
} function risky(): void {
try {
throw new Error("something broke");
} catch (error) {
console.log("recovered:", (error as Error).message);
}
}
risky();
console.log("still running"); Go separates errors (expected, returned as values) from panics (unexpected, recovered only via
defer/recover). TypeScript makes no such distinction — every failure is a thrown value caught by try/catch, so the same mechanism handles both a validation failure and a programming bug. There is nothing like Go's deliberate two-tier model; discipline about what you throw replaces it.Async & Concurrency
Goroutines vs async functions
package main
import (
"fmt"
)
func fetch(url string) string {
return "data from " + url
}
func main() {
result := fetch("https://example.com")
fmt.Println(result)
} async function fetchData(url: string): Promise<string> {
return `data from ${url}`;
}
async function main(): Promise<void> {
const result = await fetchData("https://example.com");
console.log(result);
}
main(); Go achieves concurrency with goroutines — cheap threads scheduled across CPU cores by the runtime. TypeScript (on Node.js) is single-threaded with an event loop:
async functions return a Promise<T> and await suspends without blocking the thread. This is concurrency without parallelism — many operations in flight, but one line of code running at a time. There is no go keyword and no shared-memory race condition to guard against.Channels vs promises
package main
import "fmt"
func main() {
results := make(chan int, 1)
go func() {
results <- 42 // send on a goroutine
}()
value := <-results // receive (blocks)
fmt.Println(value)
} function compute(): Promise<number> {
return new Promise((resolve) => {
resolve(42);
});
}
async function main(): Promise<void> {
const value = await compute();
console.log(value);
}
main(); A Go channel is a typed conduit between goroutines: one side sends, the other receives and blocks until a value arrives. A TypeScript
Promise is a one-shot future — it resolves exactly once with a value, and await reads it. A channel can carry many values over time and synchronizes two running goroutines; a promise carries a single eventual result with no second thread involved.Waiting on many tasks
package main
import (
"fmt"
"sync"
)
func main() {
var waitGroup sync.WaitGroup
results := make([]int, 3)
for index := 0; index < 3; index++ {
waitGroup.Add(1)
go func(position int) {
defer waitGroup.Done()
results[position] = position * 10
}(index)
}
waitGroup.Wait()
fmt.Println(results)
} async function task(position: number): Promise<number> {
return position * 10;
}
async function main(): Promise<void> {
const results = await Promise.all([task(0), task(1), task(2)]);
console.log(results);
}
main(); Go coordinates a fleet of goroutines with a
sync.WaitGroup — increment, Done, and Wait. TypeScript's Promise.all does the equivalent in one call: it takes an array of promises and resolves to an array of their results, in order, once all complete (rejecting immediately if any throws). It is the everyday tool for running independent async operations concurrently and collecting their outputs.Select vs race
package main
import "fmt"
func main() {
fast := make(chan string, 1)
slow := make(chan string, 1)
fast <- "fast result"
select {
case message := <-fast:
fmt.Println(message)
case message := <-slow:
fmt.Println(message)
}
} function after(label: string, delay: number): Promise<string> {
return new Promise((resolve) => setTimeout(() => resolve(label), delay));
}
async function main(): Promise<void> {
const winner = await Promise.race([
after("fast result", 10),
after("slow result", 50),
]);
console.log(winner);
}
main(); Go's
select waits on several channel operations and proceeds with whichever is ready first. TypeScript's Promise.race settles as soon as the first promise in the array settles, ignoring the rest — the closest analogue for "take whichever finishes first". Use Promise.any when you want the first successful result and are willing to ignore rejections.Modules
Packages vs modules
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.ToUpper("hello"))
} // Named imports pull specific exports from a module:
import { readFile } from "node:fs/promises";
// A default import binds the module's default export:
import express from "express";
const app = express();
console.log(typeof readFile, typeof app); Go imports whole packages by path and qualifies every use with the package name (
strings.ToUpper). TypeScript imports named bindings out of a module with import { x } from "...", bringing the names directly into scope, plus an optional default export. Because this example relies on Node and npm modules that the in-browser runtime cannot load, it is marked non-runnable.Exporting
// file: mathutil/mathutil.go
package mathutil
// Exported: capitalized name
func Double(value int) int {
return value * 2
}
// unexported: lowercase, package-private
func helper() int {
return 0
} // file: mathutil.ts
// Each "export" makes a name importable from another file.
export function double(value: number): number {
return value * 2;
}
// No "export" keyword: private to this module.
function helper(): number {
return 0;
}
export const VERSION = "1.0"; Go decides export visibility by capitalization across a whole package directory. TypeScript decides it per file (each file is a module) with an explicit
export keyword on whatever should be importable; everything else stays module-private. Names without export are the equivalent of Go's lowercase identifiers. This example is multi-file and uses module syntax, so it is non-runnable in the browser sandbox.Importing everything
package main
import (
m "math"
"fmt"
)
func main() {
// Aliased import: refer to the package as m.
fmt.Println(m.Sqrt(81))
} // Namespace import: gather all exports under one object.
import * as math from "./mathutil.js";
// Aliased named import with "as":
import { double as twice } from "./mathutil.js";
console.log(math.VERSION, twice(21)); Go can alias an import (
m "math") to shorten or disambiguate a package name. TypeScript offers two related tools: import * as math gathers every export of a module into one namespace object (much like Go's package-qualified access), and import { double as twice } renames an individual binding. As a multi-file module example, it is marked non-runnable here.