Generics
TypeScript generics vs Go generics
Section titled “TypeScript generics vs Go generics”TypeScript generics are pervasive — you write Array<T>, Promise<T>, Record<K, V> every day. Go added generics in 1.18 with a similar syntax but a different constraint model.
The key difference: TypeScript uses structural typing for generics (T extends { length: number }). Go uses constraint interfaces — a named interface that lists the exact set of types (or method sets) allowed. This makes Go generics more explicit but also more predictable at the cost of verbosity.
// TypeScript — structural constraintfunction identity<T>(value: T): T { return value;}
function first<T extends { length: number }>(arr: T[]): T { return arr[0];}
// Generic containerclass Stack<T> { private items: T[] = []; push(item: T) { this.items.push(item); } pop(): T | undefined { return this.items.pop(); }}// Go — type parameter + constraint interfacefunc Identity[T any](value T) T { return value}
func First[T any](slice []T) T { return slice[0]}
// Generic stacktype Stack[T any] struct { items []T}func (s *Stack[T]) Push(item T) { s.items = append(s.items, item)}func (s *Stack[T]) Pop() (T, bool) { var zero T if len(s.items) == 0 { return zero, false } last := s.items[len(s.items)-1] s.items = s.items[:len(s.items)-1] return last, true}Constraint interfaces
Section titled “Constraint interfaces”The any constraint means “any type at all” — the generic equivalent of interface{}. But Go lets you write richer constraints using interfaces that enumerate underlying types with the ~ operator.
// TypeScript — union type constrainttype Numeric = number | bigint;
function add<T extends Numeric>(a: T, b: T): T { return (a as any) + (b as any) as T;}// Go — constraint interface with type uniontype Number interface { ~int | ~int32 | ~int64 | ~float64}
func Add[T Number](a, b T) T { return a + b}
// Usagex := Add(1, 2) // inty := Add(1.5, 2.5) // float64The ~int syntax means “any type whose underlying type is int” — so your custom type type MyID int satisfies ~int. Without ~ only the exact int type would match.
The comparable constraint
Section titled “The comparable constraint”TypeScript can compare anything with ===. Go’s == operator only works on types that are comparable (no slices, maps, or functions). The built-in comparable constraint expresses this:
// TypeScript — == works on anythingfunction contains<T>(arr: T[], val: T): boolean { return arr.some(x => x === val);}// Go — comparable constraint required for ==func Contains[T comparable](slice []T, val T) bool { for _, v := range slice { if v == val { return true } } return false}
// Works for int, string, struct with comparable fields — but NOT for slices or maps.Combining method sets with type unions
Section titled “Combining method sets with type unions”A constraint interface can include both method requirements AND type unions:
type Stringer interface { String() string}
// Accept any type that is int OR float64 AND has a String() method// (rare but valid in library code)type StringableNumber interface { ~int | ~float64 String() string}In practice, most constraints are either pure method sets (classic interfaces) or pure type unions (numeric/ordered operations).
Real-world example: Map and Filter
Section titled “Real-world example: Map and Filter”// TypeScript — stdlib has theseconst doubled = [1,2,3].map(x => x * 2);const evens = [1,2,3,4].filter(x => x % 2 === 0);// Go — before 1.21 you wrote these yourselffunc Map[T, U any](s []T, fn func(T) U) []U { out := make([]U, len(s)) for i, v := range s { out[i] = fn(v) } return out}
func Filter[T any](s []T, fn func(T) bool) []T { var out []T for _, v := range s { if fn(v) { out = append(out, v) } } return out}
// Since Go 1.23 slices.Collect + iter package handles this more idiomaticallyGeneric methods and friends (Go 1.26–1.27)
Section titled “Generic methods and friends (Go 1.26–1.27)”TypeScript has always allowed a method to declare its own type parameter (class Box { map<U>(fn: (v: T) => U) }). Go could not — until 1.27. Two smaller additions from 1.26 round out the picture.
// TypeScript: method-level type parameterclass Box<T> { constructor(public value: T) {} map<U>(fn: (v: T) => U): Box<U> { return new Box(fn(this.value)); }}const n = new Box(21).map(v => v * 2); // Box<number>const s = n.map(v => v + "!"); // Box<string>// Go 1.27+: methods may declare their own type parameterstype Box[T any] struct{ Value T }
func (b Box[T]) Map[U any](fn func(T) U) Box[U] { return Box[U]{Value: fn(b.Value)}}
n := Box[int]{21}.Map(func(v int) int { return v * 2 }) // Box[int]s := n.Map(func(v int) string { return fmt.Sprint(v, "!") }) // Box[string]
// Rule: interface methods cannot declare type parameters,// so a generic method never satisfies an interface.
// Go 1.26+: new() takes an initial value (handy for *int / *string fields)type User struct{ Age *int }u := User{Age: new(42)}
// Go 1.26+: a constraint may refer to the type being constrainedtype Adder[A Adder[A]] interface{ Add(A) A }func Sum[A Adder[A]](x, y A) A { return x.Add(y) }The stdlib already uses generic methods: math/rand/v2’s (*Rand).N[Int intType](Int). Because interfaces cannot carry them, generic methods stay a convenience for concrete types — they are not a route to Java-style generic interfaces.
Generics vs interfaces — when to use which
Section titled “Generics vs interfaces — when to use which”Try it
Section titled “Try it”package main
import "fmt"
type Number interface { ~int | ~float64}
func Sum[T Number](nums []T) T { var total T for _, n := range nums { total += n } return total}
func Map[T, U any](slice []T, fn func(T) U) []U { result := make([]U, len(slice)) for i, v := range slice { result[i] = fn(v) } return result}
func Contains[T comparable](slice []T, val T) bool { for _, v := range slice { if v == val { return true } } return false}
func main() { ints := []int{1, 2, 3, 4, 5} fmt.Println("Sum:", Sum(ints)) fmt.Println("Sum floats:", Sum([]float64{1.1, 2.2, 3.3}))
doubled := Map(ints, func(x int) int { return x * 2 }) fmt.Println("Doubled:", doubled)
labels := Map(ints, func(x int) string { return fmt.Sprintf("item-%d", x) }) fmt.Println("Labels:", labels)
fmt.Println("Contains 3?", Contains(ints, 3)) fmt.Println("Contains 9?", Contains(ints, 9))}Compiling on go.dev (Go 1.27)… offline? an in-browser runtime (~8 MB, Go 1.26) is used instead