Type inference means Zig can figure out a type from the value you write.
Instead of writing this:
const age: i32 = 30;you can often write this:
const age = 30;Zig looks at the value 30 and works out what type it should have.
This keeps simple code short. You do not need to write obvious types everywhere.
A first example
const std = @import("std");
pub fn main() void {
const name = "Zig";
const year = 2026;
const stable = false;
std.debug.print("{s} {}\n", .{ name, year });
std.debug.print("stable release: {}\n", .{stable});
}Zig can infer the types of name, year, and stable.
The code does not say:
const stable: bool = false;because false is already clearly a boolean.
The code does not say:
const name: some_string_type = "Zig";because string literal types are more detailed than beginners need at first. Zig can infer them for local values.
Inference is not guessing
Type inference is not magic. Zig does not guess randomly.
It follows rules.
This value is a boolean:
const ok = true;This value is a floating point literal:
const pi = 3.14;This value is a string literal:
const language = "Zig";This value is an array:
const numbers = [_]u8{ 1, 2, 3 };The compiler reads the expression on the right side and assigns a type to the name on the left side.
You can still write the type
Type inference is optional.
You can write the type explicitly whenever it improves clarity.
const age: u8 = 30;
const score: i32 = 100;
const price: f64 = 19.99;
const enabled: bool = true;This is often better when the exact type matters.
For example, this tells the reader that age is stored as an unsigned 8-bit integer:
const age: u8 = 30;This tells the reader that price uses 64-bit floating point:
const price: f64 = 19.99;Good Zig code uses both styles. It infers types when the type is obvious, and writes types when the type is important.
Integer literals are special
Integer literals are flexible.
const x = 10;At first, 10 can behave like a compile-time integer. It can later fit into a specific integer type if the value is valid for that type.
For example:
const a: u8 = 10;
const b: i32 = 10;
const c: u64 = 10;The same literal 10 can fit into all of these types.
But this fails:
const x: u8 = 300; // errorThe type u8 can store only values from 0 to 255.
So Zig accepts integer literals only when the value fits the target type.
Floating point literals are also flexible
Floating point literals can also be used with different floating point types.
const a: f32 = 3.14;
const b: f64 = 3.14;The value is converted to the target floating point type.
When precision matters, write the type explicitly:
const distance: f64 = 12345.6789;This avoids leaving an important representation choice hidden.
Inference from function return types
Zig can infer a local variable’s type from a function call.
fn add(a: i32, b: i32) i32 {
return a + b;
}
pub fn main() void {
const result = add(10, 20);
_ = result;
}The function add returns i32, so result has type i32.
You do not need to write:
const result: i32 = add(10, 20);unless you want to make it extra clear.
Inference from arrays
Arrays often use inference.
const numbers = [_]u8{ 10, 20, 30 };The [_]u8 part means:
array of u8 values, with length inferredZig counts the elements and creates an array of length 3.
The type is:
[3]u8So this:
const numbers = [_]u8{ 10, 20, 30 };means:
create an array of 3 u8 valuesYou can also write the length manually:
const numbers: [3]u8 = .{ 10, 20, 30 };Both forms are useful.
Inference with var
Type inference works with var too.
var count = 0;But be careful.
A mutable variable must have a concrete type. If you later assign a value that does not fit, the code fails.
var count: u8 = 0;
count = 255; // ok
count = 256; // errorWhen using var, it is often clearer to write the type:
var count: usize = 0;This is common for indexes and counters.
Inference does not mean type changes
Once a variable has a type, that type does not change.
var x: i32 = 10;
x = 20; // ok
x = true; // errorThe name x was declared as i32. It can hold another i32, but it cannot suddenly become a boolean.
Even when the type is inferred, the rule is the same:
var x = @as(i32, 10);
x = 20; // ok
x = true; // errorA variable has one type for its whole lifetime.
Using @as
The builtin @as tells Zig to treat a value as a specific type.
const x = @as(u32, 10);This means:
make x a u32 with value 10This is useful when Zig needs more type information.
Example:
const std = @import("std");
pub fn main() void {
const x = @as(u8, 42);
std.debug.print("{}\n", .{x});
}Here, x is clearly a u8.
When inference is helpful
Inference is helpful for local, obvious values.
const name = "Ada";
const enabled = true;
const result = add(1, 2);These are easy to read without explicit types.
Inference is also useful when the type is long or noisy.
const std = @import("std");You do not want to write the full type of std. Zig handles it.
When explicit types are better
Write the type when it is part of the meaning.
const port: u16 = 8080;
const file_size: u64 = 1_000_000;
const index: usize = 0;
const temperature: f64 = 21.5;These types communicate intent.
A port number is usually a 16-bit unsigned integer.
An index is commonly usize.
A temperature using decimal measurement is reasonably f64.
Explicit types are also better in public interfaces:
pub fn connect(port: u16) void {
_ = port;
}Function parameters and return types should be clear because other code depends on them.
Do not fight the compiler
Sometimes beginners try to force inference too far.
This may be unclear:
var value = undefined;Zig cannot infer a useful type from undefined alone.
Write the type:
var value: i32 = undefined;Better yet, initialize it immediately when possible:
const value: i32 = 42;Zig wants enough information to prove the program is well-typed.
A complete example
const std = @import("std");
fn square(x: i32) i32 {
return x * x;
}
pub fn main() void {
const name = "Zig";
const version_major: u8 = 0;
const version_minor: u8 = 16;
var counter: usize = 0;
counter += 1;
const result = square(12);
std.debug.print("{s} version {}.{}\n", .{
name,
version_major,
version_minor,
});
std.debug.print("counter = {}\n", .{counter});
std.debug.print("square = {}\n", .{result});
}In this example, some values use inference:
const name = "Zig";
const result = square(12);Some values use explicit types:
const version_major: u8 = 0;
const version_minor: u8 = 16;
var counter: usize = 0;Both choices are deliberate.
The main idea
Type inference lets Zig remove unnecessary type annotations without weakening the type system.
The compiler still knows the exact type. The program is still statically typed. The variable still cannot change into another kind of value.
Use inference when the type is obvious. Write the type when size, meaning, API clarity, or memory layout matters.