Type reflection means asking questions about a type while Zig is compiling the program.
Type reflection means asking questions about a type while Zig is compiling the program.
In Zig, types are values at compile time. That means code can inspect a type, make decisions from it, and use those decisions to generate normal runtime code.
The main tool for this is:
@typeInfo(T)where T is a type.
For example:
const info = @typeInfo(i32);This asks Zig:
What kind of type is i32?The answer is compile-time information about the type.
Types Are Compile-Time Values
In Zig, a type can be passed to a function at compile time.
fn printTypeName(comptime T: type) void {
_ = T;
}The parameter:
comptime T: typemeans:
T is a type known at compile timeYou can call it like this:
printTypeName(i32);
printTypeName(bool);
printTypeName([]const u8);Each call passes a type, not a runtime value.
This idea is important. Zig does not need a separate template language for many generic patterns. It lets you use normal Zig code with compile-time types.
@TypeOf
The builtin @TypeOf gives you the type of an expression.
const x: i32 = 123;
const T = @TypeOf(x);Here, T is the type i32.
You can use this in generic code:
fn sameType(a: anytype, b: @TypeOf(a)) void {
_ = a;
_ = b;
}The second parameter must have the same type as the first one.
This works:
sameType(@as(i32, 1), @as(i32, 2));This does not:
sameType(@as(i32, 1), @as(u32, 2)); // error@TypeOf lets the function express relationships between types.
@typeInfo
The builtin @typeInfo tells you what kind of type something is.
const info = @typeInfo(i32);The result is a tagged union. That means it has different cases for different kinds of types.
An integer type has integer information.
A pointer type has pointer information.
A struct type has struct information.
An array type has array information.
A function type has function information.
You usually inspect it with switch.
fn describe(comptime T: type) []const u8 {
return switch (@typeInfo(T)) {
.int => "integer",
.float => "float",
.bool => "bool",
.pointer => "pointer",
.array => "array",
.@"struct" => "struct",
else => "other",
};
}Then:
const a = describe(i32);
const b = describe(f64);
const c = describe([]const u8);At compile time, Zig chooses the correct branch.
Why Some Field Names Use @"struct"
You may notice this syntax:
.@"struct"instead of:
.structThat is because struct is a Zig keyword.
When a field name or enum tag has the same spelling as a keyword, Zig uses quoted identifier syntax.
So this:
.@"struct"means the tag named struct.
You may see the same style with other keyword-like names.
A Simple Reflection Function
Here is a full example:
const std = @import("std");
fn typeName(comptime T: type) []const u8 {
return switch (@typeInfo(T)) {
.int => "integer",
.float => "float",
.bool => "boolean",
.pointer => "pointer",
.array => "array",
.@"struct" => "struct",
else => "other",
};
}
pub fn main() void {
std.debug.print("{s}\n", .{typeName(i32)});
std.debug.print("{s}\n", .{typeName(f64)});
std.debug.print("{s}\n", .{typeName(bool)});
}This prints:
integer
float
booleanThe function typeName does not inspect runtime values. It inspects compile-time types.
Reflecting on Integer Types
Integer types have information such as signedness and bit count.
const info = @typeInfo(i32);For an integer, the info includes:
signedness
bitsA simplified example:
const std = @import("std");
fn printIntInfo(comptime T: type) void {
const info = @typeInfo(T);
switch (info) {
.int => |int_info| {
std.debug.print("bits: {}\n", .{int_info.bits});
std.debug.print("signedness: {}\n", .{int_info.signedness});
},
else => @compileError("expected an integer type"),
}
}
pub fn main() void {
printIntInfo(i32);
printIntInfo(u64);
}The function accepts a type, checks whether it is an integer, then reads integer metadata.
If someone calls:
printIntInfo(bool);the function reports a compile-time error.
Reflecting on Arrays
An array type has a length and a child type.
const T = [4]u8;This type means:
array of 4 u8 valuesReflection can inspect that:
const std = @import("std");
fn arrayLength(comptime T: type) usize {
return switch (@typeInfo(T)) {
.array => |array_info| array_info.len,
else => @compileError("expected an array type"),
};
}
pub fn main() void {
const n = arrayLength([4]u8);
std.debug.print("length: {}\n", .{n});
}The result is:
length: 4The array length is known at compile time, so the function can return it as a compile-time-known value.
Reflecting on Pointers and Slices
Slices are represented through pointer information.
For example:
[]const u8is a slice type.
You can inspect whether a pointer type is a slice:
const std = @import("std");
fn isSlice(comptime T: type) bool {
return switch (@typeInfo(T)) {
.pointer => |ptr_info| ptr_info.size == .slice,
else => false,
};
}
pub fn main() void {
std.debug.print("{}\n", .{isSlice([]const u8)});
std.debug.print("{}\n", .{isSlice(*const u8)});
}This prints:
true
falseBoth types involve pointers, but they are not the same shape.
[]const u8is a slice: pointer plus length.
*const u8is a single-item pointer.
Reflection lets generic code tell the difference.
Reflecting on Struct Fields
Reflection is especially useful with structs.
Suppose we have:
const User = struct {
id: u64,
name: []const u8,
active: bool,
};You can inspect the fields of User at compile time.
const std = @import("std");
const User = struct {
id: u64,
name: []const u8,
active: bool,
};
fn printStructFields(comptime T: type) void {
const info = @typeInfo(T);
switch (info) {
.@"struct" => |struct_info| {
inline for (struct_info.fields) |field| {
std.debug.print("{s}\n", .{field.name});
}
},
else => @compileError("expected a struct type"),
}
}
pub fn main() void {
printStructFields(User);
}This prints:
id
name
activeThe loop is:
inline for (struct_info.fields) |field| {It runs at compile time. Zig unrolls the loop while compiling.
This is one of the main reasons reflection matters in Zig. You can inspect a type and generate code from its fields.
inline for and Reflection
A normal for loop runs at runtime.
An inline for loop is expanded at compile time.
When you loop over type metadata, you usually need inline for.
inline for (struct_info.fields) |field| {
// compile-time field information
}This lets Zig generate code for each field.
For example, a serializer might inspect a struct and generate code to write each field.
A logger might inspect a struct and print field names.
A validation function might check whether required fields exist.
Building a Simple Field Counter
Here is a small function that counts fields in a struct:
fn fieldCount(comptime T: type) usize {
return switch (@typeInfo(T)) {
.@"struct" => |struct_info| struct_info.fields.len,
else => @compileError("expected a struct type"),
};
}Usage:
const User = struct {
id: u64,
name: []const u8,
active: bool,
};
const count = fieldCount(User);The result is:
3This happens at compile time because User is known at compile time.
Reflection Helps Generic Code Stay Type-Safe
Reflection is often used with anytype.
For example, a generic function may accept many values, but only allow arrays and slices:
fn lenOf(value: anytype) usize {
const T = @TypeOf(value);
return switch (@typeInfo(T)) {
.array => value.len,
.pointer => |ptr_info| {
if (ptr_info.size == .slice) {
return value.len;
}
@compileError("lenOf expects an array or slice");
},
else => @compileError("lenOf expects an array or slice"),
};
}This function works for arrays:
const a = [_]u8{ 1, 2, 3 };
const n = lenOf(a);It also works for slices:
const s: []const u8 = "hello";
const m = lenOf(s);But it rejects integers, structs without .len, and other unrelated values with a clear compile-time error.
Reflection Is Not Runtime Introspection
In many languages, reflection means inspecting objects while the program runs.
Zig reflection is mainly compile-time reflection.
That means:
The compiler inspects types.
The compiler generates code.
The final program runs the generated code.This keeps runtime behavior simple.
There is no heavy runtime reflection system hidden inside every value.
A normal Zig value does not carry a large metadata object around at runtime.
Reflection and Public APIs
Reflection is powerful, but it can make code harder to read.
For beginner-level code, prefer explicit types.
Use reflection when it removes real repetition or makes a generic API safer.
Good use:
generic serialization
generic formatting
field validation
type-safe containers
compile-time adaptersPoor use:
making simple code clever
hiding ordinary field access
accepting any type when one concrete type would be clearerZig gives you reflection, but it expects you to use it deliberately.
The Main Idea
Type reflection lets Zig code inspect types at compile time.
The common tools are:
@TypeOf(value)
@typeInfo(T)Use @TypeOf when you need the type of an expression.
Use @typeInfo when you need details about a type.
Together, they let you write generic code that is still checked by the compiler.
The key mental model is:
Zig reflection happens mostly while compiling, not while running.That is why it fits Zig’s style. It gives you powerful generic programming without turning the runtime into a dynamic reflection system.