Skip to content

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.

The syntax is similar: square brackets for type parameters.

TypeScript
// TypeScript — structural generic
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
// Works for any type — no explicit constraint needed
first([1, 2, 3]); // number
first(["a", "b"]); // string
first([{ id: 1 }]); // object
// Constrained generic
function 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
// Go — type parameter with explicit constraint
func 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 interface
type 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' constraint
func Contains[T comparable](slice []T, item T) bool {
for _, v := range slice {
if v == item { return true }
}
return false
}

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
// TypeScript — conditional type for numeric union
type 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
// Go — interface constraint with type union
type 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 float64
temps := []Celsius{20, 21, 22}
fmt.Println(Sum(temps)) // works — ~float64 includes Celsius

Go’s type system is deliberately simpler. Three things TypeScript developers commonly reach for that do not exist in Go:

TypeScript
// TypeScript — advanced generic features Go lacks
// 1. Conditional types
type IsString<T> = T extends string ? "yes" : "no";
type R1 = IsString<string>; // "yes"
type R2 = IsString<number>; // "no"
// 2. Mapped types
type 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 types
type EventName = `on${Capitalize<string>}`;
Go
// 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.
TypeScript
// TypeScript — map and filter are built into Array
const doubled = [1, 2, 3].map(n => n * 2);
const evens = [1, 2, 3].filter(n => n % 2 === 0);
Go
// 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 })
package main
import "fmt"
// Number constraint — ~int means any type with underlying int
type Number interface {
~int | ~float64
}
// Sum works for any Number type
func 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 params
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
}
// 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))
}
Which constraint allows using == between two values of a generic type in Go?
What does ~int in a Go type constraint mean?
Which TypeScript generic feature does Go NOT have?
In Go generics, what does "any" mean as a constraint?