Skip to content

`@intCast`

@intCast converts one integer value to another integer type.

@intCast

@intCast converts one integer value to another integer type.

You use it when Zig needs an explicit promise that the integer value fits in the destination type.

const small: u8 = @intCast(big);

This says:

Convert big to u8.

But the value must fit in u8.

A u8 can store values from 0 to 255.

Why @intCast Exists

Integer conversions can lose information.

Example:

const big: u16 = 300;
const small: u8 = big;

This is not allowed.

Why? Because 300 cannot fit in u8.

If Zig silently allowed this, the program could produce a wrong value.

So Zig requires an explicit cast:

const big: u16 = 200;
const small: u8 = @intCast(big);

This is allowed because 200 fits in u8.

The Destination Type Comes from Context

In modern Zig, @intCast does not take the destination type as an argument.

You write:

const small: u8 = @intCast(big);

The destination type is u8, because the variable says:

const small: u8

You can also provide the type with @as:

const small = @as(u8, @intCast(big));

This means the same thing, but it is more explicit.

Compile-Time Checking

If the compiler knows the value does not fit, it rejects the code.

const x: u16 = 300;
const y: u8 = @intCast(x);

Since x is known at compile time, Zig can see that 300 does not fit in u8.

A valid example:

const x: u16 = 200;
const y: u8 = @intCast(x);

This works.

Runtime Checking

Sometimes the value is known only when the program runs.

fn shrink(x: u16) u8 {
    return @intCast(x);
}

This function says: convert x to u8.

But not every u16 fits in u8.

In safe build modes, Zig checks the value at runtime. If the value is too large, the program traps.

A safer version returns an error:

fn shrink(x: u16) !u8 {
    if (x > 255) return error.TooLarge;
    return @intCast(x);
}

Now the function handles the problem directly.

Signed and Unsigned Integers

@intCast also handles signedness changes.

Example:

const x: i32 = 100;
const y: u32 = @intCast(x);

This works because 100 can be represented as u32.

This does not work safely:

const x: i32 = -1;
const y: u32 = @intCast(x);

A negative value cannot be represented as an unsigned integer.

A safe version checks first:

fn toUnsigned(x: i32) !u32 {
    if (x < 0) return error.Negative;
    return @intCast(x);
}

Widening Usually Does Not Need @intCast

If the destination type can represent every value of the source type, Zig can often convert without @intCast.

Example:

const small: u8 = 200;
const big: u16 = small;

Every u8 value fits in u16, so this is safe.

But narrowing needs a cast:

const big: u16 = 200;
const small: u8 = @intCast(big);

Narrowing means converting to a type with a smaller range.

@intCast Is Not @truncate

@intCast checks that the value fits.

@truncate keeps only the low bits and discards the rest.

Example:

const x: u16 = 300;
const y: u8 = @truncate(x);

300 in hexadecimal is:

0x012c

The low 8 bits are:

0x2c

So y becomes 44.

That is not a safe numeric conversion. It is bit-level truncation.

Use @intCast when you want the same numeric value in a different integer type.

Use @truncate when you intentionally want to discard high bits.

Common Example: Indexes

Many lengths and indexes use usize.

But sometimes an API expects a smaller integer type.

fn writeByteCount(count: usize) !u8 {
    if (count > 255) return error.TooLarge;
    return @intCast(count);
}

This is clear:

The input may be large.
The output must fit in one byte.
The function checks before casting.

Common Example: File Formats

Binary file formats often use fixed-size integer fields.

For example, a format may store a length as u16.

fn encodeLength(len: usize) !u16 {
    if (len > std.math.maxInt(u16)) {
        return error.LengthTooLarge;
    }

    return @intCast(len);
}

This avoids silent overflow.

The cast is correct because the function checks the range first.

Using std.math.maxInt

Instead of writing 255, use std.math.maxInt for clarity:

const std = @import("std");

fn shrink(x: u16) !u8 {
    if (x > std.math.maxInt(u8)) return error.TooLarge;
    return @intCast(x);
}

This is better because the limit is tied to the destination type.

For signed types, you may also use std.math.minInt.

const std = @import("std");

fn toI8(x: i32) !i8 {
    if (x < std.math.minInt(i8)) return error.TooSmall;
    if (x > std.math.maxInt(i8)) return error.TooLarge;
    return @intCast(x);
}

@intCast Does Not Change Meaning

@intCast is a numeric conversion.

It tries to preserve the number.

Example:

const x: u16 = 42;
const y: u8 = @intCast(x);

Both x and y mean the number 42.

This is different from @bitCast, which preserves raw bits.

For integer type changes, prefer @intCast unless you truly need bit-level behavior.

How to Read @intCast

When you see:

const y: T = @intCast(x);

read it as:

Convert integer x to type T, and require that the value fits.

Then ask:

Can every possible x fit in T?
If not, where is the range check?
Should this function return an error instead of trapping?

That habit prevents many integer bugs.

Key Idea

@intCast converts an integer to another integer type while preserving the numeric value.

It is used when the conversion may be unsafe without a range check.

Use it for explicit integer narrowing or signedness changes. Check the range yourself when invalid input is possible.