Type coercion means Zig converts a value from one type to another when the conversion is safe and well-defined.
Type coercion means Zig converts a value from one type to another when the conversion is safe and well-defined.
For example:
const x: u8 = 10;
const y: u16 = x;Here, x is a u8.
y is a u16.
Zig allows this because every u8 value can fit inside a u16. A u8 can hold values from 0 to 255. A u16 can hold values from 0 to 65535.
No information is lost.
Coercion Is Not Guessing
Zig does not silently convert values in risky ways.
This is not allowed:
const x: u16 = 1000;
const y: u8 = x; // errorA u16 might contain a value too large for u8.
Even though 1000 is clearly too large, the deeper rule is this: Zig does not let a wider integer automatically become a narrower integer.
If you really want a narrowing conversion, you must say so explicitly.
const x: u16 = 200;
const y: u8 = @intCast(x);Now the cast is visible in the code.
Coercion vs Casting
Coercion is automatic.
Casting is explicit.
Coercion:
const small: u8 = 10;
const large: u16 = small;Casting:
const large: u16 = 10;
const small: u8 = @intCast(large);The difference matters.
Coercion means the compiler knows the conversion is safe.
Casting means the programmer is asking for a specific conversion.
Good Zig code lets safe conversions stay simple and makes risky conversions visible.
Integer Coercion
Integer coercion is allowed when the destination type can represent every possible value of the source type.
This works:
const a: u8 = 255;
const b: u16 = a;
const c: u32 = b;This also works:
const a: i8 = -10;
const b: i16 = a;An i8 can hold values from -128 to 127. An i16 can hold all of those values.
But this does not work:
const a: i16 = 100;
const b: i8 = a; // errorA normal i16 might not fit into an i8.
This also does not work automatically:
const a: i32 = -1;
const b: u32 = a; // errorSigned and unsigned integer conversions can change meaning. Zig requires explicit code.
Compile-Time Integers
Integer literals are special.
This works:
const x: u8 = 100;The literal 100 has no fixed runtime integer type yet. Zig can check at compile time that it fits into u8.
This does not work:
const x: u8 = 300; // error300 cannot fit into u8.
So literals are flexible, but still checked.
This is allowed:
const a: u8 = 10;
const b: u16 = 1000;
const c: i32 = -50;Zig chooses the destination type from context and checks the value.
Float Coercion
Floating-point coercion can happen from a smaller float type to a larger float type.
const x: f32 = 1.5;
const y: f64 = x;An f64 can represent at least as much precision and range as an f32.
The reverse is not automatic:
const x: f64 = 1.5;
const y: f32 = x; // errorTo narrow a float, use an explicit cast:
const x: f64 = 1.5;
const y: f32 = @floatCast(x);Again, Zig makes the potentially lossy conversion visible.
Integer to Float
Zig does not freely mix integers and floats.
This is not allowed:
const x: i32 = 10;
const y: f32 = x; // errorUse an explicit conversion:
const x: i32 = 10;
const y: f32 = @floatFromInt(x);Going the other way also requires explicit code:
const x: f32 = 10.8;
const y: i32 = @intFromFloat(x);This is important because float-to-integer conversion can lose fractional data.
10.8 becomes 10Zig does not hide that.
Pointer Coercion
Some pointer conversions are safe and automatic.
For example, a mutable pointer can become a const pointer:
var number: i32 = 42;
const mutable_ptr: *i32 = &number;
const const_ptr: *const i32 = mutable_ptr;This is safe because *const i32 is more restrictive. It promises not to mutate through that pointer.
But the reverse is not allowed automatically:
const number: i32 = 42;
const const_ptr: *const i32 = &number;
const mutable_ptr: *i32 = const_ptr; // errorThat would remove a safety restriction.
Zig allows coercion when moving toward a safer, more restrictive type. It rejects coercion when it would give code more power than it had before.
Array to Slice Coercion
Arrays can coerce to slices.
const numbers = [_]i32{ 1, 2, 3, 4 };
const slice: []const i32 = numbers[0..];The expression:
numbers[0..]creates a slice that views the whole array.
A slice contains a pointer and a length. It does not copy the array.
You can pass an array slice to a function:
fn sum(items: []const i32) i32 {
var total: i32 = 0;
for (items) |item| {
total += item;
}
return total;
}
pub fn main() void {
const numbers = [_]i32{ 1, 2, 3, 4 };
_ = sum(numbers[0..]);
}The function does not need to know the array length at compile time. It receives a slice.
String Literal Coercion
A string literal in Zig has an array-like type.
"hello"In many contexts, it can be used as:
[]const u8Example:
const name: []const u8 = "Zig";This is why string literals are convenient in functions that expect byte slices:
fn greet(name: []const u8) void {
std.debug.print("hello, {s}\n", .{name});
}
pub fn main() void {
greet("Zig");
}The string literal is stored as constant bytes, and Zig lets it be viewed as a constant byte slice.
Optional Coercion
A plain value can coerce into an optional type.
const x: i32 = 10;
const maybe_x: ?i32 = x;This is safe. An i32 value can become “maybe an i32” by storing the value case.
You can also write:
const maybe_name: ?[]const u8 = "Zig";The string is wrapped into the optional.
But the reverse is not automatic:
const maybe_x: ?i32 = 10;
const x: i32 = maybe_x; // errorThe optional might be null.
You must unwrap it:
const x: i32 = maybe_x orelse 0;or:
const x: i32 = maybe_x.?;Error Union Coercion
A successful value can coerce into an error union.
const result: error{Failed}!i32 = 123;This means the result is the success case.
An error can also be stored in the same error union:
const result: error{Failed}!i32 = error.Failed;But you cannot automatically turn an error union into its payload:
const result: error{Failed}!i32 = 123;
const value: i32 = result; // errorThe result might be an error.
You must handle it:
const value: i32 = result catch 0;or propagate it:
const value: i32 = try result;Error Set Coercion
A smaller error set can coerce into a larger error set.
const Small = error{
NotFound,
};
const Large = error{
NotFound,
PermissionDenied,
};
fn small() Small!void {
return error.NotFound;
}
fn large() Large!void {
return try small();
}This works because every Small error is also valid in Large.
But the reverse does not work safely:
fn returnsLarge() Large!void {
return error.PermissionDenied;
}
fn returnsSmall() Small!void {
return try returnsLarge(); // error
}Large may contain errors that Small cannot represent.
Comptime Coercion
Compile-time values can often coerce more flexibly because Zig knows their exact value.
const x: u8 = 255;This works.
const y: u8 = 256; // errorThis fails.
Zig is not guessing. It is checking the exact value at compile time.
This also applies inside functions when a value is known at compile time.
fn make(comptime n: comptime_int) u8 {
return n;
}Calling:
const a = make(100);is fine.
Calling:
const b = make(300);fails because 300 cannot fit in u8.
Coercion Should Preserve Meaning
The best way to understand Zig coercion is this:
A coercion is allowed when it preserves meaning.
A u8 value as a u16 still means the same number.
A mutable pointer as a const pointer is still the same address, but with fewer permissions.
A value as an optional is still the same value, just wrapped in a type that can also hold null.
But a u16 as a u8 may lose data.
A f64 as an f32 may lose precision.
An optional as a plain value may crash if it is null.
An error union as a plain value may ignore failure.
Zig makes those conversions explicit.
The Main Idea
Type coercion is Zig’s safe automatic conversion system.
It keeps simple, safe code clean:
const small: u8 = 10;
const large: u16 = small;But it rejects conversions that may lose information, hide failure, remove const safety, or ignore absence.
For those, you must write an explicit cast, unwrap, try, or catch.
That is the rule to remember:
Safe conversions may be automatic.
Risky conversions must be explicit.