`Gene` is unboxed. Any `int` behaves like `Gene`. An `array` of `Gene` is tightly packed. Any `proc` of `Gene` will instantiate at compile time to the concrete types of `Gene`. The `seq[Gene]` or the `table` must contain one concrete type. There is zero runtime overhead. You have zero type safety.
`Gene2` is boxed. Any `x` of type `int` will have to be wrapped `Gene2(kind:GeneInt, num:x)`. An `array` of `Gene2(kind:GeneInt,...)` is much larger than an `array` of `int`. Any `proc` of `Gene2` will need to check the `kind` at runtime. The `seq` or the `table` can contain mixed types, `int`, `string`, `seq`, or `table` wrapped under `Gene2`. That contributes runtime overhead. You have type safety. There is also one middle ground. You get type safety, without heterogeneity. Nim type GeneInt = distinct int GeneString = distinct string GeneSeq = seq[Gene3] GeneMap = Table[string, Gene3] Gene3 = GeneInt | GeneString | GeneSeq | GeneMap Run I have the feeling, possibly biased, that you don't really understand what you are asking for. It will be really useful if you start with concrete types, and write all the `proc` for each type separately. You can design the generic interface later by calling concrete `proc`. TLDR: If you only want a uniform interface, but not type safety, go with `Gene`. If, in addition to a uniform interface, you want type safety, go with `Gene3`. If you really want heterogeneous arrays and don't care about the runtime overhead, go with `Gene2`.