Zig Type Coercion
Type coercion is the automatic conversion of one type to another. Zig performs a small, well-defined set of safe coercions automatically. Everything else requires an explicit cast. This strict separation prevents the silent data corruption that happens in languages where numbers freely convert between types without any indication in the code.
Coercion vs Casting
Coercion (automatic, always safe): const x: u8 = 10; const y: u16 = x; // u8 fits safely in u16, Zig allows automatically Casting (explicit, may lose data): const big: u32 = 70_000; const small: u16 = @truncate(big); // you must ask explicitly // small = 4464 (70000 mod 65536) — data lost, but you chose to do it
Automatic coercions: smaller int → larger int u8 → u16 → u32 → u64 signed compat i8 → i16 → i32 → i64 array → slice [5]u8 → []u8 const escalation T → const T optional wrapping T → ?T error union wrapping T → E!T comptime literals comptime_int → any int type
Integer Widening
A value from a smaller integer type fits safely into a larger one. Zig coerces this automatically:
const small: u8 = 200;
const wide: u16 = small; // 200 fits in u16, coercion automatic
const wider: u32 = wide; // 200 fits in u32, coercion automatic
std.debug.print("{d}\n", .{wider}); // 200
u8 (0–255)
└──coerces to──► u16 (0–65535)
└──coerces to──► u32 (0–4 billion)
└──► u64 ...
The value 200 fits in all of these. No data lost.
Comptime Integer Literals
Number literals in Zig are comptime integers — they have no fixed type until they are used in a context that determines one. They coerce to whichever integer type is needed, as long as the value fits:
const a: i32 = 42; // comptime_int 42 → i32 const b: u8 = 42; // comptime_int 42 → u8 const c: f64 = 42; // comptime_int 42 → f64 const d: u8 = 256; // ERROR: 256 does not fit in u8
Pointer Coercions
*T coerces to *const T (adding const is always safe) [N]T coerces to []T (array to slice) [N:0]T coerces to [:0]T (sentinel array to sentinel slice) *[N]T coerces to []T (pointer to array to slice)
var arr = [_]u8{ 1, 2, 3 };
const slice: []u8 = &arr; // *[3]u8 → []u8
const cslice: []const u8 = &arr; // adds const
Optional Coercion
Any non-optional value coerces automatically to the optional version of its type. The optional simply wraps the value:
const x: i32 = 42;
const opt: ?i32 = x; // i32 coerces to ?i32 automatically
// opt is now ?i32 holding 42
fn findIndex(haystack: []const u8, needle: u8) ?usize {
for (haystack, 0..) |byte, i| {
if (byte == needle) return i; // usize coerces to ?usize
}
return null;
}
return i → usize value 3
coerces to → ?usize holding 3
return null → ?usize holding nothing
Error Union Coercion
A plain value coerces to an error union type automatically — it becomes the success case:
fn divide(a: f64, b: f64) !f64 {
if (b == 0) return error.DivByZero;
return a / b; // f64 coerces to !f64 (success)
}
What Does NOT Coerce Automatically
DOES NOT coerce (must cast explicitly): u16 → u8 (might truncate) i32 → u32 (sign conversion) f64 → f32 (might lose precision) f64 → i32 (drops decimal, changes meaning) u8 → bool (different kind entirely)
const big: u32 = 300;
const small: u8 = big; // COMPILE ERROR: u32 cannot coerce to u8
// Use @truncate or @intCast with a check
Explicit Casting Functions
@as(TargetType, value) → reinterpret value as TargetType @intCast(value) → convert int, panic if out of range @truncate(value) → cut bits off, no panic @floatCast(value) → convert float, may lose precision @floatFromInt(int_value) → int to float @intFromFloat(float_value) → float to int (truncates toward zero) @intFromEnum(enum_value) → enum variant to its integer tag @enumFromInt(int_value) → integer to enum variant @ptrCast(pointer) → reinterpret pointer type
const f: f64 = 9.7; const i: i32 = @intFromFloat(f); // i = 9 (truncated, not rounded) const n: i32 = -5; const u: u32 = @intCast(n); // PANIC in debug: -5 cannot be u32 const u2: u32 = @bitCast(n); // reinterprets bits: u2 = 4294967291
Practical Example: Mixing Integer Sizes
const std = @import("std");
pub fn main() void {
const byte_count: u8 = 100;
const total_kb: u32 = byte_count; // u8 → u32 coercion (safe)
const big_num: u32 = 70_000;
const clamped: u16 = if (big_num <= 65535)
@intCast(big_num)
else
65535;
const ratio: f64 = @as(f64, @floatFromInt(byte_count)) / 1024.0;
std.debug.print("bytes={d}, kb={d}, clamped={d}, ratio={d:.4}\n",
.{byte_count, total_kb, clamped, ratio});
}
Output:
bytes=100, kb=100, clamped=65535, ratio=0.0977
Summary Table
Conversion │ Automatic? │ Function to use ─────────────────────────┼────────────┼────────────────────── u8 → u32 │ Yes │ (none needed) u32 → u8 │ No │ @intCast or @truncate i32 → f64 │ No │ @floatFromInt f64 → i32 │ No │ @intFromFloat T → ?T │ Yes │ (none needed) T → E!T │ Yes │ (none needed) [N]T → []T │ Yes │ (none needed) enum → int │ No │ @intFromEnum int → enum │ No │ @enumFromInt
