Lesson 15 / 25
Reflection with @typeInfo and anytype
Inspect types at compile time to write flexible code.
Code that adapts to types
@TypeOf(x) returns the type of an expression, and @typeInfo(T) returns a description of a type as a tagged union (std.builtin.Type): for a struct, its fields with names, types and defaults; for an enum, its fields and values; for an integer, its signedness and bit count; and so on. Recent Zig versions name these variants with lowercase keywords, written as .@"struct", .@"enum" and .int. Combined with inline for over fields and @field(value, name) to access a field by a compile-time name, this enables compile-time reflection: serialisers, debug printers, argument parsers, ORMs and validation can be written once for any struct, with no run-time reflection cost. anytype parameters accept any type and are specialised per call; use them for flexible APIs, but document the expected shape, and prefer explicit comptime T: type when the type matters for readability. Helpers such as std.meta.fields, std.meta.FieldEnum and std.meta.eql wrap common patterns. Keep reflection-heavy code in libraries, as it can make compile errors harder to read.
A generic struct printer and validator
inline for over fields, @field and @compileError.
const std = @import("std");
fn printStruct(value: anytype) void {
const T = @TypeOf(value);
const info = @typeInfo(T);
if (info != .@"struct") @compileError("printStruct expects a struct, got " ++ @typeName(T));
std.debug.print("{s} {{\n", .{@typeName(T)});
inline for (info.@"struct".fields) |field| {
const v = @field(value, field.name);
if (field.type == []const u8) {
std.debug.print(" {s}: \"{s}\"\n", .{ field.name, v });
} else {
std.debug.print(" {s}: {any}\n", .{ field.name, v });
}
}
std.debug.print("}}\n", .{});
}
fn countZeroFields(value: anytype) usize {
var zeros: usize = 0;
inline for (std.meta.fields(@TypeOf(value))) |field| {
if (@typeInfo(field.type) == .int) { // comptime-known: other fields skip this branch
if (@field(value, field.name) == 0) zeros += 1;
}
}
return zeros;
}
const Product = struct {
sku: []const u8,
price_paise: u32,
stock: u16,
active: bool,
};
pub fn main() void {
const p = Product{ .sku = "pen", .price_paise = 4_950, .stock = 0, .active = true };
printStruct(p);
std.debug.print("zero integer fields: {d}\n", .{countZeroFields(p)}); // 1
// printStruct(42); // compile error: printStruct expects a struct, got comptime_int
}Escape braces in format strings
Zig's formatter treats { and } as placeholders, so print literal braces as {{ and }}, as in the struct printer above.
Quick check: What does @field(value, name) do when name is comptime-known?
- Creates a new field at run time
- Deletes a field
- Accesses the field of value with that name, resolved at compile time
- Returns the field's type only
Answer
Accesses the field of value with that name, resolved at compile time — @field enables reflection-driven access with no run-time lookup.