# Type T as an array index or hashmap key

**URL:** <https://ziggit.dev/t/type-t-as-an-array-index-or-hashmap-key/17662>\
**Category:** Help\
**Tags:** language\
**Created:** [September 19, 2026, 7:27pm UTC](https://ziggit.dev/t/type-t-as-an-array-index-or-hashmap-key/17662 "2026-09-19T19:27:49Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![lemuriaResident](https://ziggit.dev/letter_avatar_proxy/v4/letter/l/a8b319/32.png) [@lemuriaResident](https://ziggit.dev/u/lemuriaResident)\
**Post date:** [September 19, 2026, 7:27pm UTC](https://ziggit.dev/t/type-t-as-an-array-index-or-hashmap-key/17662/1 "2026-09-19T19:27:49Z")

</div>

While perusing cpp code I’ve noticed two patterns I’m interested in,  
first the less interesting:

# Q1: Type T as a hashmap key

In cpp they use `std::type_index(typeid(T))`, closest thing in zig I  
know of is `@typeName(T)`, which can be used as a key of  
`std.StringHashMap`. But as someone who knows nothing about how  
hashmaps works, I am convinced an `usize` would be much more efficient  
than a `[]const u8`. I actually got something to work, only anonymous  
structs survive optimization, but it is quite goofy and might stop  
working at any time I guess.

Question: Is there any better way and does avoiding  
`std.StringHashMap` make sense ? These systems get fetched every frame  
of a game, I’d imagine it makes a difference, and that’s also why I  
would avoid hashmaps completly in favour of Q2.

```zig
//! zig version: 0.16.0

const std = @import("std");

const AudioSystem = struct {
    // pub const Index: usize = 0;
    // optimization kills the option above, only anonymous structs work
    pub const Index = struct { x: u8 = 0 }{};
};

const RenderSystem = struct {
    pub const Index = struct { x: u8 = 0 }{};
};

const SystemManager = struct {
    map: std.AutoHashMap(usize, usize),

    fn put(self: *@This(), T: anytype, val: usize) !void {
        try self.map.put(@intFromPtr(&T.Index), val);
    }

    fn get(self: @This(), T: anytype) ?usize {
        return self.map.get(@intFromPtr(&T.Index));
    }
};

pub fn main(init: std.process.Init) !void {
    var manager: SystemManager = .{ .map = .init(init.arena.allocator()) };
    try manager.put(AudioSystem, 67);
    try manager.put(RenderSystem, 420);

    std.debug.print(
        \\audio {?d} {*}
        \\render {?d} {*}
        \\
    , .{
        manager.get(AudioSystem), &AudioSystem.Index,
        manager.get(RenderSystem), &RenderSystem.Index,
    });
}

```

Output:

```sh
$ zig build-exe -O ReleaseFast test.zig ; ./test
audio 67 test.AudioSystem.Index__struct_2267@100ec68
render 420 test.RenderSystem.Index__struct_2281@100ec69

```

# Q2: Type T as an index into an array

Blah blah blah something called static methods apparently, hard to  
understand cppnese. Every Component T should know its index into an  
array, where a pool of said components is stored.

The only solution I could think of:

```zig
const CompId = enum(usize) {
    hp,
    mana,
};

const ManaComp = struct {
    pub const Id: usize = @intFromEnum(CompId.mana);
};

const HpComp = struct {
    pub const Id: usize = @intFromEnum(CompId.hp);
};

fn getId(T: anytype) usize {
    return T.Id;
}

```

Question: This works, but is there some magic where a function at  
comptime could return a global incrementing counter to populate these?

---

<div class="post-metadata">

**Author:** ![abdullah-b-al](https://ziggit.dev/user_avatar/ziggit.dev/abdullah-b-al/32/4601_2.png) [@abdullah-b-al](https://ziggit.dev/u/abdullah-b-al)\
**Post date:** [September 19, 2026, 8:42pm UTC](https://ziggit.dev/t/type-t-as-an-array-index-or-hashmap-key/17662/2 "2026-09-19T20:42:19Z")

</div>

There is a bit of a hybrid approach where you use `std.StaticStringMap` built during comptime to produce and get indices.  
You’ll have to manually add all the types you’re interested in, but all `get` operations are pre-computed:

```zig
const type_index_map = blk: {
    const Tuple = struct { []const u8, usize };
    var kvs: []const Tuple = &.{};
    for (.{
        @typeName(ManaComp),
        @typeName(HpComp),
    }, 0..) |name, i| {
        const v: Tuple = .{ name, i };
        kvs = kvs ++ .{v};
    }

    break :blk std.StaticStringMap(u32).initComptime(kvs);
};

pub fn type_index(comptime T: type) u32 {
    return comptime type_index_map.get(@typeName(T)).?;
}

```

---

<div class="post-metadata">

**Author:** ![npc1054657282](https://ziggit.dev/user_avatar/ziggit.dev/npc1054657282/32/5790_2.png) [@npc1054657282](https://ziggit.dev/u/npc1054657282)\
**Post date:** [September 19, 2026, 9:14pm UTC](https://ziggit.dev/t/type-t-as-an-array-index-or-hashmap-key/17662/3 "2026-09-19T21:14:49Z")

</div>

> [@lemuriaResident](#):
>
> `@typeName(T)`, which can be used as a key of

A bit of a shame, but actually it can’t do it.

related

> <https://github.com/ziglang/zig/issues/19858#issuecomment-2369861301>
>
> Add a new builtin called \`@typeId\`:
> 
> \`\`\`zig
> @typeId(comptime T: type) usize
> …\`\`\`
> 
> This builtin returns a unique integer for each type passed, and will return the same integer for the same type.
> 
> The return value must not be consistent inbetween builds, so a second build might return completly different numbers
> for the same types.
> 
> An alternative variant might return \`u32\` or \`u64\` to have a stable interface between different platforms.
> 
> \## Use cases
> 
> \- Type erasure
> \- Additional programmatic type safety
> \- Variadic in-memory serialization
> 
> \## Prior art:
> 
> \- \[any-pointer\](https://github.com/MasterQ32/any-pointer/blob/4d4794b2063b66b7fe0d7575ac1942e61a154f88/any-pointer.zig#L118-L135)
> 
> \## User-land implementation
> 
> The following version is runtime only, as we can't perform intFromPtr at compiletime:
> 
> \`\`\`zig
> fn typeId(comptime T: type) TypeId {
> const Tag = struct {
> var name: u8 = @typeName(T)\[0\]; // must depend on the type somehow!
> inline fn id() TypeId {
> return @enumFromInt(@intFromPtr(&name));
> }
> };
> return Tag.id();
> }
> \`\`\`

---

<div class="post-metadata">

**Author:** ![alanza](https://ziggit.dev/user_avatar/ziggit.dev/alanza/32/6006_2.png) [@alanza](https://ziggit.dev/u/alanza)\
**Post date:** [September 19, 2026, 11:58pm UTC](https://ziggit.dev/t/type-t-as-an-array-index-or-hashmap-key/17662/4 "2026-09-19T23:58:52Z")

</div>

Try `AutoHashMap` if you don’t want to write the hash function yourself.

---

<div class="post-metadata">

**Author:** ![Sze](https://ziggit.dev/user_avatar/ziggit.dev/sze/32/496_2.png) [@Sze](https://ziggit.dev/u/Sze)\
**Post date:** [September 20, 2026, 8:41am UTC](https://ziggit.dev/t/type-t-as-an-array-index-or-hashmap-key/17662/5 "2026-09-20T08:41:38Z")

</div>

This reminds me a bit of this topic:

> [@How to use cached types in runtime?](https://ziggit.dev/t/how-to-use-cached-types-in-runtime/12849/9):
>
> Here is a slightly different implementation that keeps the same API and is a bit simpler and more efficient (not needing to lookup the typed arrays for every push) internally. Keeping the API is pretty constraining, but if this is how you want to use it then I think we can’t do much better, this pretty much hardcodes that there will only be one event system, the benefit from this is that you don’t have to pass the event system instance or allocator everywhere. (Personally I don’t like singleton…

related topics:

- [Type id comptime generation](https://ziggit.dev/t/type-id-comptime-generation/10956)
- [Pre-comptime collection of "distributed enumeration"?](https://ziggit.dev/t/pre-comptime-collection-of-distributed-enumeration/11223)

* * *

I think my preferred solution is to have something like this (if all your components are known at comptime):

```zig
const ManaComp = struct {};

const HpComp = struct {};

const NewComp = struct {}; // not registered yet

const ComponentId = enum(u32) { _ };
pub fn componentId(comptime ComponentType: type) ComponentId {
    // NOTE linear search here should be un-problematic unless you have a huge number of components
    // because the comptime result of this function is memoized anyway
    return @enumFromInt(comptime std.mem.findScalar(type, &components, ComponentType) orelse @compileError("please add missing Component: " ++ @typeName(ComponentType) ++ " to the components array"));
}
const components = [_]type{
    HpComp,
    ManaComp,
    // NewComp,
};

const std = @import("std");

pub fn main() !void {
    std.debug.print("HpComp id: {}\n", .{componentId(HpComp)});
    std.debug.print("ManaComp id: {}\n", .{componentId(ManaComp)});
    std.debug.print("NewComp id: {}\n", .{componentId(NewComp)});
}

```

A good question to ask yourself is whether you need components which are registered at runtime, if you don’t need them you can take advantage of simple comptime code like this, where you just add all components to this `components` array (getting an error if you forget) and you get a stable and reliable id for each component in return.

So basically this gives you a reliable index for your type, simply by enforcing that your type is part of this specific array that is used to determine / declare its index.

---

<div class="post-metadata">

**Author:** ![lemuriaResident](https://ziggit.dev/letter_avatar_proxy/v4/letter/l/a8b319/32.png) [@lemuriaResident](https://ziggit.dev/u/lemuriaResident)\
**Post date:** [September 20, 2026, 2:01pm UTC](https://ziggit.dev/t/type-t-as-an-array-index-or-hashmap-key/17662/6 "2026-09-20T14:01:21Z")

</div>

@Sze  
Thank you this is exactly what I wanted, wasn’t aware you can keep types in an comptime array.

Ended up with the code below to mimic static methods, I like it, the only issue I’ve had with the enum approach were the inevitable copy-paste bugs, impossible with this one.

```zig
const ManaComp = struct {
    pub const Id: usize = componentId(@This());
};

```

@npc1054657282  
Very good to know, thank you, of course it would truncate the name at some point haven’t thought of that.
