In Zig, a type is a value.
This is one of the most important ideas behind compile-time programming.
A type can be passed to a function, stored in a constant, compared, and returned from a function.
The special type named type represents all types.
const T: type = i32;Here T is a value known at compile time. Its value is the type i32.
A function can receive a type parameter:
fn printType(comptime T: type) void {
std.debug.print("{s}\n", .{@typeName(T)});
}Call it like this:
printType(i32);
printType(bool);
printType(f64);The output is:
i32
bool
f64The parameter:
comptime T: typemeans:
Tis known during compilation- the value stored in
Tis itself a type
This allows functions to work with types directly.
A function may also return a type.
fn IntArray(comptime N: usize) type {
return [N]i32;
}The call:
const A = IntArray(4);makes A equal to:
[4]i32So this declaration:
var data: A = .{ 1, 2, 3, 4 };is the same as:
var data: [4]i32 = .{ 1, 2, 3, 4 };The function constructed a type during compilation.
This idea appears throughout Zig.
A generic container is usually a function returning a type.
fn Pair(comptime T: type) type {
return struct {
first: T,
second: T,
};
}Use it like this:
const IntPair = Pair(i32);
var p = IntPair{
.first = 10,
.second = 20,
};The compiler creates a struct where both fields are i32.
Another call:
const FloatPair = Pair(f64);creates a different type.
The language does not need a separate template system for this.
Types are ordinary compile-time values.
This also means types can participate in control flow.
fn choose(comptime flag: bool) type {
if (flag)
return i32;
return f64;
}Then:
const A = choose(true);
const B = choose(false);makes:
A == i32
B == f64A type may be inspected with builtin functions.
const std = @import("std");
pub fn main() void {
std.debug.print("{d}\n", .{@sizeOf(i32)});
std.debug.print("{d}\n", .{@alignOf(f64)});
}Typical output:
4
8@sizeOf and @alignOf operate on type values.
Zig also allows reflection on types.
const std = @import("std");
fn describe(comptime T: type) void {
switch (@typeInfo(T)) {
.int => std.debug.print("integer\n", .{}),
.float => std.debug.print("float\n", .{}),
.bool => std.debug.print("boolean\n", .{}),
else => std.debug.print("other\n", .{}),
}
}
pub fn main() void {
describe(i32);
describe(f64);
describe(bool);
}The output is:
integer
float
boolean@typeInfo returns structural information about a type.
This allows programs to generate specialized behavior during compilation.
For example, a formatting function might handle integers differently from floating-point values.
Because types are values, Zig generic code tends to remain compact.
This function swaps two values:
fn swap(comptime T: type, a: *T, b: *T) void {
const tmp = a.*;
a.* = b.*;
b.* = tmp;
}The same function works for many types.
var x: i32 = 10;
var y: i32 = 20;
swap(i32, &x, &y);Or:
var a: bool = true;
var b: bool = false;
swap(bool, &a, &b);The compiler generates code specialized for each type.
Types as values also make APIs explicit.
Instead of hidden compiler rules or implicit template deduction, the type appears directly in the call:
swap(i32, &x, &y)The reader can see immediately which type is being used.
This style matches Zig’s general design: explicit operations, visible control flow, and minimal hidden behavior.
Exercise 10-9. Write a function Array(comptime T: type, comptime N: usize) type returning [N]T.
Exercise 10-10. Write a generic reverse function for arrays of any type.
Exercise 10-11. Use @sizeOf and @alignOf to print information about several types.
Exercise 10-12. Write a function isInteger(comptime T: type) bool using @typeInfo.