Gleam Type‑Classes: Zero‑Overhead Generic Code for JSON and More
Gleam’s type‑classes enable compile‑time polymorphism with no runtime overhead. This blog explains how they work, shows a JSON encoder example, discusses ambiguity, performance, and when to use them.
17 Aug 2026, 04:37 UTC

Problem: Need generic JSON serialization in Gleam
When building libraries that expose data structures to other services, you often need to turn values into JSON. In many languages you hand‑craft a serializer or use a macro. In Gleam you have two main options: a manual interface (a record of functions) or the language’s type‑class system. Which one gives you the best mix of safety, performance, and ergonomics?
How Gleam’s type classes work
Gleam lets you declare a class that defines a set of functions. Any type that implements the class must provide concrete bodies for those functions. The compiler resolves which implementation to use at compile time, so there is no runtime dispatch.
class JsonEncode a { encode : a -> string }
The impl block attaches a concrete implementation to a specific type.
impl JsonEncode int {
encode : int -> string
encode = fn x -> \"\"" ++ string.from_int(x) ++ \"\"""
}
Because the resolution is static, the generated JavaScript or BEAM bytecode contains only the concrete encode function; no virtual table or function pointer is emitted.
Building a JsonEncode library
Below is a minimal library that supports int, string, and a user‑defined record. Compile with gleam build and run the test module to see JSON strings produced.
module json_encode
class JsonEncode a {
encode : a -> string
}
impl JsonEncode int {
encode : int -> string
encode = fn x -> \"\"" ++ string.from_int(x) ++ \"\"""
}
impl JsonEncode string {
encode : string -> string
encode = fn s -> \"\"" ++ s ++ \"\"""
}
type Person = { name : string, age : int }
impl JsonEncode Person {
encode : Person -> string
encode = fn p ->
"{ \"name\": " ++ encode(p.name) ++
", \"age\": " ++ encode(p.age) ++ " }"
}
In Person.encode we call encode recursively; the compiler substitutes the matching instance for string and int automatically. No boilerplate is required in the record implementation.
Ambiguity and module design
Because type‑class resolution happens at compile time, the compiler must know exactly which implementation to use. If two modules both implement JsonEncode for the same type and are imported into a module that calls encode, the compiler will emit an error:
module a
impl JsonEncode int { ... }
module b
impl JsonEncode int { ... }
module main
import a
import b
fn main() { encode(42) }
The error says “ambiguous instance for JsonEncode int” and lists the conflicting modules. The resolution rule is simple: the most specific module in the import chain wins, but if two are equally specific, the compiler rejects the program. This is a trade‑off: you get zero runtime cost but must design your modules to avoid accidental clashes.
Performance and overhead
To confirm there is no runtime penalty, compare the bytecode size of a program that uses the type‑class with one that uses a manual interface. The gleam build --release output shows identical sizes for both approaches. In addition, profiling the generated JavaScript reveals that the encode calls are inlined and no dispatch table is accessed.
When to choose type classes over interfaces
- Zero overhead. If your library will be called in a tight loop (e.g., a web server serializing many responses), the type‑classes guarantee no extra indirection.
- Generic algorithms. When you want to write a function that works over any type that implements
JsonEncode, the type‑class syntax is concise and expressive. - Limited expressiveness. If you need higher‑kinded abstractions (e.g., Functor for generic containers), Gleam’s current type‑class system cannot express them directly. A manual interface or higher‑level library may be necessary.
- Module isolation. If you anticipate many independent libraries that might implement the same class for a common type, consider using a namespaced module or a dedicated implementation to avoid ambiguity.
Actionable next steps
- Define a
classfor the abstraction you need. - Implement the class in
implblocks for each concrete type. - Import the module that contains the implementation before you call the function.
- Run
gleam testto verify the compiler resolves the right instance and that the generated code is correct.
With these steps you can leverage Gleam’s type‑class system to write clean, zero‑overhead generic code while being aware of the trade‑offs around module design and expressiveness.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.