Zig does not have a formal trait or interface system.
Instead, generic code is constrained by the operations it performs. A type is valid if the required operations exist for that type.
Consider this function:
fn add(a: anytype, b: @TypeOf(a)) @TypeOf(a) {
return a + b;
}This function does not explicitly require numeric types.
Instead, it assumes the + operator is valid.
These calls work:
const x = add(3, 4);
const y = add(1.5, 2.25);because integers and floating-point values support addition.
This call fails:
const z = add(true, false);The compiler reports an error because booleans do not support +.
The constraint comes from use.
The compiler checks operations only after the function is instantiated with concrete types.
This style appears throughout Zig code.
Here is a generic equality function:
fn equal(a: anytype, b: @TypeOf(a)) bool {
return a == b;
}The only requirement is that the type supports ==.
The compiler enforces this automatically.
A more realistic example is sorting.
fn lessThan(a: anytype, b: @TypeOf(a)) bool {
return a < b;
}This works for ordered numeric types.
It fails for structs unless the struct defines meaningful comparison logic elsewhere.
Sometimes the requirements should be checked explicitly.
Zig provides compile-time reflection for this purpose.
This function accepts only integer types:
const std = @import("std");
fn double(value: anytype) @TypeOf(value) {
const T = @TypeOf(value);
const info = @typeInfo(T);
switch (info) {
.int => {},
else => @compileError("expected integer type"),
}
return value * 2;
}
pub fn main() void {
const x = double(21);
std.debug.print("{d}\n", .{x});
}The output is:
42The line:
@typeInfo(T)returns information about the type.
The switch checks whether the type category is .int.
If not, the compiler stops with:
expected integer typeThis produces clearer diagnostics than waiting for an invalid operator later in the function.
Compile-time checks can enforce more detailed rules.
This function accepts only pointer types:
fn isNull(ptr: anytype) bool {
const T = @TypeOf(ptr);
switch (@typeInfo(T)) {
.pointer => {},
else => @compileError("expected pointer"),
}
return ptr == null;
}Generic constraints may also depend on declarations.
Suppose a function requires a type to contain a method named write:
fn callWrite(writer: anytype) void {
const T = @TypeOf(writer);
if (!@hasDecl(T, "write")) {
@compileError("type must provide write");
}
writer.write();
}@hasDecl checks whether a declaration exists in the type.
This resembles interface checking, but the mechanism is entirely compile-time reflection.
The standard library frequently uses this approach.
Containers, allocators, formatters, and I/O abstractions often require types to provide specific declarations or operations.
The constraints are structural:
- does the type support this operation?
- does this declaration exist?
- does this function return the expected type?
No explicit inheritance hierarchy is required.
This keeps the language small while still allowing highly generic code.
Exercise 11-9. Write a generic function that accepts only floating-point types.
Exercise 11-10. Write a function that accepts only pointer types.
Exercise 11-11. Write a generic sum function for integer arrays.
Exercise 11-12. Write a compile-time check that requires a type to contain a declaration named init.