Skip to content

`@typeInfo`

@typeInfo asks the compiler for structured information about a type.

@typeInfo

@typeInfo asks the compiler for structured information about a type.

Example:

const info = @typeInfo(u32);

This means:

Tell me what kind of type u32 is.

The answer is not a string. It is a compile-time value that describes the type.

For u32, the information says: this is an integer type, it is unsigned, and it has 32 bits.

@typeInfo Takes a Type

This is the normal form:

const info = @typeInfo(T);

T must be a type.

For example:

const a = @typeInfo(u8);
const b = @typeInfo(i64);
const c = @typeInfo([]const u8);

If you have a value and want to inspect its type, combine @TypeOf and @typeInfo:

const x = 123;
const info = @typeInfo(@TypeOf(x));

Read this as:

Get the type of x.
Then get information about that type.

A Simple Example

const std = @import("std");

pub fn main() void {
    const info = @typeInfo(u32);

    switch (info) {
        .int => |int_info| {
            std.debug.print("integer bits: {}\n", .{int_info.bits});
            std.debug.print("signedness: {}\n", .{int_info.signedness});
        },
        else => {
            std.debug.print("not an integer\n", .{});
        },
    }
}

Possible output:

integer bits: 32
signedness: builtin.Signedness.unsigned

The switch checks what kind of type u32 is.

Since u32 is an integer type, the .int branch runs.

Type Information Is Tagged

@typeInfo returns a tagged value.

That means the result has a category, such as:

int
float
bool
pointer
array
struct
enum
union
optional
error_union
function
void

Each category carries different information.

An integer has a bit count and signedness.

A pointer has a child type, alignment, constness, and pointer size.

An array has a child type and length.

A struct has fields and declarations.

So @typeInfo does not return the same shape for every type. You must switch on the type category first.

Inspecting an Integer

const std = @import("std");

fn describeInt(comptime T: type) void {
    const info = @typeInfo(T);

    switch (info) {
        .int => |int_info| {
            std.debug.print("bits: {}\n", .{int_info.bits});
            std.debug.print("signed: {}\n", .{int_info.signedness == .signed});
        },
        else => {
            std.debug.print("not an integer type\n", .{});
        },
    }
}

pub fn main() void {
    describeInt(u8);
    describeInt(i32);
}

Possible output:

bits: 8
signed: false
bits: 32
signed: true

The function takes a type at compile time:

fn describeInt(comptime T: type) void

That is necessary because type information exists at compile time.

Inspecting an Array

Arrays have a length and a child type.

const std = @import("std");

fn describeArray(comptime T: type) void {
    const info = @typeInfo(T);

    switch (info) {
        .array => |array_info| {
            std.debug.print("array length: {}\n", .{array_info.len});
            std.debug.print("element size: {}\n", .{@sizeOf(array_info.child)});
        },
        else => {
            std.debug.print("not an array\n", .{});
        },
    }
}

pub fn main() void {
    describeArray([5]u16);
}

Possible output:

array length: 5
element size: 2

For [5]u16, the array length is 5, and the child type is u16.

Inspecting a Pointer

Pointers carry more information.

const std = @import("std");

fn describePointer(comptime T: type) void {
    const info = @typeInfo(T);

    switch (info) {
        .pointer => |ptr_info| {
            std.debug.print("is const: {}\n", .{ptr_info.is_const});
            std.debug.print("child size: {}\n", .{@sizeOf(ptr_info.child)});
        },
        else => {
            std.debug.print("not a pointer\n", .{});
        },
    }
}

pub fn main() void {
    describePointer(*u32);
    describePointer(*const u32);
}

Possible output:

is const: false
child size: 4
is const: true
child size: 4

*u32 and *const u32 both point to a u32, but the second one points to data that should not be modified through that pointer.

@typeInfo can see that difference.

Inspecting a Struct

Structs contain fields.

const std = @import("std");

const Point = struct {
    x: i32,
    y: i32,
};

fn describeStruct(comptime T: type) void {
    const info = @typeInfo(T);

    switch (info) {
        .@"struct" => |struct_info| {
            inline for (struct_info.fields) |field| {
                std.debug.print("field: {s}, size: {}\n", .{
                    field.name,
                    @sizeOf(field.type),
                });
            }
        },
        else => {
            std.debug.print("not a struct\n", .{});
        },
    }
}

pub fn main() void {
    describeStruct(Point);
}

Possible output:

field: x, size: 4
field: y, size: 4

Notice the syntax:

.@"struct"

struct is a keyword, so this form is used when matching the type information tag.

The inline for matters because struct_info.fields is compile-time information. The compiler unrolls the loop while compiling the program.

Why @typeInfo Is Useful

Most beginner programs do not need reflection.

But @typeInfo becomes useful when writing generic code.

For example, you may want a function that only accepts integer types:

fn requireInteger(comptime T: type) void {
    switch (@typeInfo(T)) {
        .int => {},
        else => @compileError("expected an integer type"),
    }
}

Then:

requireInteger(u32); // ok
requireInteger([]const u8); // compile error

This lets you enforce rules at compile time.

A Generic Example

Here is a function that returns the bit count of an integer type:

fn bitCount(comptime T: type) comptime_int {
    return switch (@typeInfo(T)) {
        .int => |int_info| int_info.bits,
        else => @compileError("bitCount expects an integer type"),
    };
}

Use it like this:

const std = @import("std");

fn bitCount(comptime T: type) comptime_int {
    return switch (@typeInfo(T)) {
        .int => |int_info| int_info.bits,
        else => @compileError("bitCount expects an integer type"),
    };
}

pub fn main() void {
    std.debug.print("u8 has {} bits\n", .{bitCount(u8)});
    std.debug.print("u64 has {} bits\n", .{bitCount(u64)});
}

Output:

u8 has 8 bits
u64 has 64 bits

This function does not run normal runtime inspection. It runs using compile-time type information.

@typeInfo and @Type

The two builtins work together.

@typeInfo takes a type and returns a description.

@Type takes a description and builds a type.

So conceptually:

const info = @typeInfo(u32);
const T = @Type(info);

T becomes u32.

In normal beginner code, you rarely need this pair. But it shows a deep Zig idea: types can be inspected, passed around, and constructed at compile time.

Do Not Use It When Simple Code Is Enough

This is simple:

fn add(a: u32, b: u32) u32 {
    return a + b;
}

Do not replace it with reflection-heavy code unless you have a reason.

@typeInfo is powerful, but it can make code harder to read.

Use it when your code truly needs to react to the shape of a type.

Good uses include:

generic containers
serialization
formatting
binary encoding
compile-time validation
testing helpers
type-safe wrappers

Poor uses include making ordinary code clever for no reason.

Key Idea

@typeInfo(T) gives compile-time information about type T.

It lets Zig code inspect integers, pointers, arrays, structs, enums, unions, optionals, functions, and other types.

For beginners, remember this simple rule:

Use @typeInfo when generic code needs to ask, “What kind of type is this?”