Skip to content

`@Type`

@Type builds a type from compile-time type information.

@Type

@Type builds a type from compile-time type information.

That sounds abstract, so start with the simple version:

const MyInt = u32;

Here, MyInt is just another name for the type u32.

But @Type lets you create a type from a structured description:

const T = @Type(.{
    .int = .{
        .signedness = .unsigned,
        .bits = 32,
    },
});

This creates the type u32.

You will not use @Type often as a beginner. It is an advanced builtin for generic code, reflection, and metaprogramming. But it is useful to understand what it does, because it completes the pair with @typeInfo.

@typeInfo breaks a type apart.

@Type builds a type from parts.

The Pair: @typeInfo and @Type

Think of the relationship like this:

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

The first line asks:

What is the structure of u32?

The second line asks:

Build a type from this structure.

So for many types, this is the rough idea:

@Type(@typeInfo(T)) == T

This means: inspect a type, then rebuild it.

You usually do not write this exact code in normal programs, but it explains the concept.

Creating an Integer Type

Here is a small example:

const std = @import("std");

const U32 = @Type(.{
    .int = .{
        .signedness = .unsigned,
        .bits = 32,
    },
});

pub fn main() void {
    const x: U32 = 123;
    std.debug.print("{}\n", .{x});
}

This program creates a 32-bit unsigned integer type.

It is equivalent to using u32.

const U32 = u32;

The @Type version is longer because it describes the type manually.

Signed and Unsigned Integers

You can build signed integer types too:

const I64 = @Type(.{
    .int = .{
        .signedness = .signed,
        .bits = 64,
    },
});

This creates i64.

The two important fields are:

.signedness
.bits

The signedness says whether the integer can represent negative numbers.

The bits field says how many bits the integer uses.

Why This Exists

At first, @Type may look unnecessary. Why write this:

const U32 = @Type(.{
    .int = .{
        .signedness = .unsigned,
        .bits = 32,
    },
});

when you can write this?

const U32 = u32;

For ordinary code, you should write the simple version.

@Type becomes useful when the type is computed.

Example: you want to create an unsigned integer type with a bit width chosen at compile time.

fn UInt(comptime bits: u16) type {
    return @Type(.{
        .int = .{
            .signedness = .unsigned,
            .bits = bits,
        },
    });
}

Now you can write:

const U7 = UInt(7);
const U13 = UInt(13);
const U128 = UInt(128);

Zig supports arbitrary-width integer types, so u7 and u13 are valid integer types.

A Working Example

const std = @import("std");

fn UInt(comptime bits: u16) type {
    return @Type(.{
        .int = .{
            .signedness = .unsigned,
            .bits = bits,
        },
    });
}

pub fn main() void {
    const U7 = UInt(7);

    const x: U7 = 100;
    std.debug.print("{}\n", .{x});
}

A 7-bit unsigned integer can store values from 0 to 127, so 100 fits.

This would not fit:

const x: U7 = 200;

The compiler rejects it because 200 is too large for 7 bits.

@Type Returns a Type

This point matters:

const U7 = UInt(7);

U7 is a type, not a value.

Then you use it as a type:

const x: U7 = 100;

This is the same pattern as:

const MyNumber = u32;
const x: MyNumber = 100;

In Zig, types can be passed around and returned at compile time.

That is one of the main reasons comptime is powerful.

Creating Array Types

@Type can also build array types.

For example:

const Bytes16 = @Type(.{
    .array = .{
        .len = 16,
        .child = u8,
        .sentinel = null,
    },
});

This creates:

[16]u8

You can use it like this:

const std = @import("std");

const Bytes16 = @Type(.{
    .array = .{
        .len = 16,
        .child = u8,
        .sentinel = null,
    },
});

pub fn main() void {
    const data: Bytes16 = [_]u8{0} ** 16;
    std.debug.print("len = {}\n", .{data.len});
}

Output:

len = 16

Again, in normal code you should usually write [16]u8. But when the length or child type is computed at compile time, @Type can be useful.

A Generic Array Type Builder

fn Array(comptime Child: type, comptime len: usize) type {
    return @Type(.{
        .array = .{
            .len = len,
            .child = Child,
            .sentinel = null,
        },
    });
}

Use it like this:

const Buffer = Array(u8, 1024);

This creates:

[1024]u8

A complete example:

const std = @import("std");

fn Array(comptime Child: type, comptime len: usize) type {
    return @Type(.{
        .array = .{
            .len = len,
            .child = Child,
            .sentinel = null,
        },
    });
}

pub fn main() void {
    const Buffer = Array(u8, 8);
    const buf: Buffer = [_]u8{0} ** 8;

    std.debug.print("{}\n", .{buf.len});
}

Creating Pointer Types

@Type can describe pointer types too.

Pointer type descriptions are more detailed because pointers have several properties:

const PtrToU8 = @Type(.{
    .pointer = .{
        .size = .one,
        .is_const = false,
        .is_volatile = false,
        .alignment = @alignOf(u8),
        .address_space = .generic,
        .child = u8,
        .is_allowzero = false,
        .sentinel = null,
    },
});

This creates a type like:

*u8

Do not worry if this feels dense. Pointer types carry a lot of information in Zig. They are not just “addresses.” They also encode constness, alignment, address space, child type, and pointer kind.

For beginner code, write *u8 directly.

Use @Type only when you need to construct such a type programmatically at compile time.

@Type Is a Metaprogramming Tool

Metaprogramming means writing code that works with code-like structures.

In Zig, types are compile-time values. @Type allows you to construct those values.

This is useful for:

generic containers

serialization libraries

binary format readers

compile-time validation

reflection systems

automatic wrappers

low-level APIs that need exact type construction

Most application code does not need @Type.

But library code sometimes uses it heavily.

Do Not Overuse It

A beginner should not reach for @Type first.

Prefer normal type syntax:

u32
[]const u8
[16]u8
*const Foo
struct { x: i32, y: i32 }

Use @Type only when the type has to be built from compile-time information.

Clear code is better than clever type construction.

This is clear:

const Buffer = [1024]u8;

This is more complex:

const Buffer = @Type(.{
    .array = .{
        .len = 1024,
        .child = u8,
        .sentinel = null,
    },
});

They describe the same type, but the first version is easier to read.

Key Idea

@Type creates a type from compile-time type information.

It is the inverse of @typeInfo.

Use @typeInfo when you want to inspect a type.

Use @Type when you want to build a type.

For beginners, the main lesson is simple: Zig treats types as compile-time values, and @Type is the builtin that can construct those values explicitly.