Skip to content

`anytype`

anytype means the function parameter can accept many different types.

anytype

anytype means the function parameter can accept many different types.

It is Zig’s simplest way to write a generic function.

Here is a normal function:

fn double(x: i32) i32 {
    return x * 2;
}

This works only with i32.

const a = double(10);

But this does not work:

const b = double(1.5); // error

The function expects an i32, not a floating-point number.

With anytype, the function can accept different types:

fn double(x: anytype) @TypeOf(x) {
    return x * 2;
}

Now x can be an integer, a float, or any other type that supports * 2.

const a = double(@as(i32, 10));
const b = double(@as(f64, 1.5));

The return type is:

@TypeOf(x)

That means the function returns the same type as x.

anytype Is Checked at Compile Time

anytype does not mean “ignore types.”

It does not make Zig dynamically typed.

Zig still checks everything at compile time.

This function accepts any type, but the body must make sense for the actual type passed in.

fn double(x: anytype) @TypeOf(x) {
    return x * 2;
}

This works:

const n = double(@as(i32, 21));

This does not:

const s = double("hello"); // error

A string cannot be multiplied by 2.

So anytype means:

Let the caller choose the type, then compile a version of this function for that type.

It does not mean:

Accept anything at runtime and hope it works.

A Simple Generic Function

Here is a generic max function:

fn max(a: anytype, b: @TypeOf(a)) @TypeOf(a) {
    if (a > b) {
        return a;
    } else {
        return b;
    }
}

The first parameter can be any type.

a: anytype

The second parameter must have the same type as the first:

b: @TypeOf(a)

The return type is also the same:

@TypeOf(a)

Now the function works with different numeric types:

const x = max(@as(i32, 10), @as(i32, 20));
const y = max(@as(f64, 1.5), @as(f64, 0.5));

But this should not work:

const z = max(@as(i32, 10), @as(u32, 20)); // error

The second argument must match the first argument’s type.

anytype Creates a Compile-Time Parameter

A parameter written as anytype is known at compile time.

This is why you can inspect its type:

fn printType(x: anytype) void {
    _ = x;
    @compileLog(@TypeOf(x));
}

If you call:

printType(@as(i32, 123));
printType(@as(bool, true));

Zig sees two different calls with two different concrete types.

Conceptually, the compiler treats them like separate versions:

printType for i32
printType for bool

This is why anytype is powerful. It lets you write one function body, while the compiler still works with real concrete types.

anytype and Duck Typing

anytype is sometimes described as compile-time duck typing.

That means the function does not name an interface. It simply uses operations, and the type must support those operations.

Example:

fn lengthOf(x: anytype) usize {
    return x.len;
}

This works for values that have a .len field:

const a = [_]u8{ 1, 2, 3 };
const n = lengthOf(a);

It also works for slices:

const s: []const u8 = "hello";
const m = lengthOf(s);

But it does not work for an integer:

const bad = lengthOf(@as(i32, 10)); // error

An i32 does not have a .len field.

The function accepts any type that has what the function uses.

That is the “duck typing” idea, but it happens at compile time.

Better Error Messages with Compile-Time Checks

A plain anytype function can produce confusing errors if the wrong type is passed.

For example:

fn lengthOf(x: anytype) usize {
    return x.len;
}

Calling it with an integer gives an error somewhere inside the function body.

You can make the error clearer with a compile-time check:

fn lengthOf(x: anytype) usize {
    const T = @TypeOf(x);
    const info = @typeInfo(T);

    switch (info) {
        .array => return x.len,
        .pointer => |ptr| {
            if (ptr.size == .slice) {
                return x.len;
            }

            @compileError("lengthOf expects an array or slice");
        },
        else => @compileError("lengthOf expects an array or slice"),
    }
}

This is more advanced, but the idea is simple:

Use @typeInfo to inspect the type.
Reject bad types with @compileError.

The caller gets a clearer message.

anytype vs comptime T: type

There are two common ways to write generic functions in Zig.

The first uses anytype:

fn identity(x: anytype) @TypeOf(x) {
    return x;
}

The second passes the type explicitly:

fn identity(comptime T: type, x: T) T {
    return x;
}

You call them differently.

With anytype:

const a = identity(@as(i32, 10));

With explicit type parameter:

const b = identity(i32, 10);

Both are generic.

Use anytype when the type can be inferred from the value.

Use comptime T: type when the caller should provide the type directly, or when the function needs the type before receiving a value.

Example: Generic Swap

Here is a generic swap function:

fn swap(a: anytype, b: @TypeOf(a)) struct { @TypeOf(a), @TypeOf(a) } {
    return .{ b, a };
}

This returns an anonymous tuple-like struct containing the values in reverse order.

const result = swap(@as(i32, 1), @as(i32, 2));

A clearer version uses an explicit type parameter:

fn swap(comptime T: type, a: T, b: T) struct { T, T } {
    return .{ b, a };
}

Call it like this:

const result = swap(i32, 1, 2);

For beginners, the explicit type version is often easier to read.

anytype is short and convenient, but explicit comptime T: type makes the generic type visible.

Example: Printing Any Value

The standard formatting system already supports generic printing, but a simple wrapper shows how anytype feels:

const std = @import("std");

fn debugPrint(value: anytype) void {
    std.debug.print("{any}\n", .{value});
}

pub fn main() void {
    debugPrint(@as(i32, 123));
    debugPrint(true);
    debugPrint("hello");
}

The same function accepts several different types.

The compiler checks each call separately.

anytype Is Not for Storing Values

anytype is used in function parameters.

You cannot use it as a normal variable type:

var x: anytype = 10; // error

This is not what anytype means.

A variable must have a specific type.

var x: i32 = 10;

or let Zig infer the specific type:

var y = @as(i32, 10);

anytype belongs in function parameter lists:

fn f(x: anytype) void {
    _ = x;
}

Avoid Overusing anytype

anytype is convenient, but it can make APIs harder to understand.

This function is clear:

fn parsePort(text: []const u8) !u16 {
    // parse text
}

This version is less clear:

fn parsePort(text: anytype) !u16 {
    // what types are allowed?
}

If the function really expects a byte slice, say so.

Use anytype when the function genuinely works across multiple types.

Do not use it just to avoid thinking about the correct type.

A Practical Rule

Use a concrete type when the function expects one kind of value.

fn countBytes(data: []const u8) usize {
    return data.len;
}

Use anytype when the function accepts a family of types and the operations are the same.

fn isEmpty(x: anytype) bool {
    return x.len == 0;
}

Use comptime T: type when the type itself is an important input.

fn makeZero(comptime T: type) T {
    return @as(T, 0);
}

The Main Idea

anytype is Zig’s compact syntax for generic function parameters.

fn f(x: anytype) void {
    _ = x;
}

It means the caller can pass different concrete types, and the compiler checks each call at compile time.

It does not remove type checking.

It does not make Zig dynamic.

It simply lets a function be written once and specialized for the actual types used by the caller.