Generics — Go vs TypeScript
TypeScript generics: structural and very flexible
Section titled “TypeScript generics: structural and very flexible”TypeScript generics use structural typing — any type that has the right shape satisfies a constraint, even without explicitly declaring it. TypeScript also has conditional types (T extends U ? X : Y), mapped types ({ [K in keyof T]: ... }), and template literal types.
Go generics (added in Go 1.18) are simpler and more explicit. Every type parameter must satisfy an interface constraint, and Go has no conditional types, mapped types, or template literal types.
Basic generic function
Section titled “Basic generic function”The syntax is similar: square brackets for type parameters.
// TypeScript — structural genericfunction first<T>(arr: T[]): T | undefined { return arr[0];}
// Works for any type — no explicit constraint neededfirst([1, 2, 3]); // numberfirst(["a", "b"]); // stringfirst([{ id: 1 }]); // object
// Constrained genericfunction getLength<T extends { length: number }>(v: T): number { return v.length; // structural: any type with .length works}getLength("hello");getLength([1, 2, 3]);getLength({ length: 5, name: "x" }); // also works — has .length// Go — type parameter with explicit constraintfunc First[T any](arr []T) (T, bool) { var zero T if len(arr) == 0 { return zero, false } return arr[0], true}
// Constrained generic — must specify the constraint interfacetype Lengther interface { Len() int // explicit method — not structural!}
// Without a constraint, you can only use operations valid for 'any':// == is NOT valid for 'any' unless you add 'comparable' constraintfunc Contains[T comparable](slice []T, item T) bool { for _, v := range slice { if v == item { return true } } return false}Union type constraints with ~
Section titled “Union type constraints with ~”Go’s constraint syntax uses interface bodies. You can list concrete types with | for type unions, and ~T means “any type whose underlying type is T” (covers custom types like type MyInt int).
// TypeScript — conditional type for numeric uniontype Numeric = number | bigint;function add<T extends Numeric>(a: T, b: T): T { return (a as any) + (b as any) as T; // needs cast — TS can't infer}// Go — interface constraint with type uniontype Number interface { ~int | ~int32 | ~int64 | ~float32 | ~float64}
func Sum[T Number](nums []T) T { var total T for _, n := range nums { total += n // + is valid because all listed types support it } return total}
type Celsius float64 // underlying type is float64temps := []Celsius{20, 21, 22}fmt.Println(Sum(temps)) // works — ~float64 includes CelsiusWhat Go generics cannot do
Section titled “What Go generics cannot do”Go’s type system is deliberately simpler. Three things TypeScript developers commonly reach for that do not exist in Go:
// TypeScript — advanced generic features Go lacks
// 1. Conditional typestype IsString<T> = T extends string ? "yes" : "no";type R1 = IsString<string>; // "yes"type R2 = IsString<number>; // "no"
// 2. Mapped typestype Optional<T> = { [K in keyof T]?: T[K] };type User = { id: number; name: string };type PartialUser = Optional<User>; // { id?: number; name?: string }
// 3. Template literal typestype EventName = `on${Capitalize<string>}`;// Go — none of these exist; alternatives:
// 1. No conditional types — use multiple functions or interfaces// Instead of IsString<T>: write two separate functions
// 2. No mapped types — use struct embedding or code generation// go generate + text/template for mechanical transformations
// 3. No template literal types — strings are just strings at runtime
// Go's philosophy: if the type system cannot express it simply,// use code generation (go generate) or accept a small amount// of repetition. Explicitness over metaprogramming.Generic Map and Filter functions
Section titled “Generic Map and Filter functions”// TypeScript — map and filter are built into Arrayconst doubled = [1, 2, 3].map(n => n * 2);const evens = [1, 2, 3].filter(n => n % 2 === 0);// Go 1.18+ — implement with generics (not in stdlib yet)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 Filter[T any](slice []T, fn func(T) bool) []T { var result []T for _, v := range slice { if fn(v) { result = append(result, v) } } return result}
doubled := Map([]int{1,2,3}, func(n int) int { return n * 2 })evens := Filter([]int{1,2,3}, func(n int) bool { return n%2==0 })Try it
Section titled “Try it”package main
import "fmt"
// Number constraint — ~int means any type with underlying inttype Number interface { ~int | ~float64}
// Sum works for any Number typefunc Sum[T Number](nums []T) T { var total T for _, n := range nums { total += n } return total}
// Map transforms a slice — T and U are independent type paramsfunc 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}
// Contains requires comparable constraint for ==func Contains[T comparable](slice []T, item T) bool { for _, v := range slice { if v == item { return true } } return false}
func main() { ints := []int{1, 2, 3, 4, 5} floats := []float64{1.1, 2.2, 3.3}
fmt.Println("sum ints: ", Sum(ints)) fmt.Println("sum floats:", Sum(floats))
doubled := Map(ints, func(n int) int { return n * 2 }) fmt.Println("doubled: ", doubled)
strs := Map(ints, func(n int) string { return fmt.Sprintf("#%d", n) }) fmt.Println("strings: ", strs)
fmt.Println("has 3:", Contains(ints, 3)) fmt.Println("has 9:", Contains(ints, 9))}Loading Go runtime (first run only, ~8 MB)…