The root of the problem is that it’s a bit different from the normal metaprogramming of Expr to Expr. Instead, you’re processing to and taking apart an atypical DataType, and .types contains TypeVars, not Symbols, for type parameters.
julia> Base.unwrap_unionall(Bus).types
svec(T)
julia> x = Base.unwrap_unionall(Bus).types[1]
T
julia> x |> dump
TypeVar
name: Symbol T
lb: Union{}
ub: Any
TypeVars have extra information in the type bounds. You normally never get access to TypeVars, but you have to be extra careful when you do.
The first reason why it doesn’t fail loudly is that Expr trees can contain any objects, not just the Symbols, Exprs, and a few literals parsed from source code. The correct type definition is obviously parseable from source code, so interpolating something else like TypeVar reasonably risks a difference.
The second reason it doesn’t fail loudly is because the bad expression evaluates to the same kind of DataType that unwrap_unionall makes:
julia> :(Vector{$x}) |> eval
Array{T, 1}
julia> :(Vector{$x}) |> eval |> typeof
DataType
Such a DataType is allowed to exist for type processing, and that’s what you annotated the field with.
The third and final reason it doesn’t fail loudly is that Julia doesn’t know to check for such atypical DataTypes. If you didn’t have a type parameter, it does complain for another reason:
julia> :(struct Y y::$x end) |> eval
ERROR: ArgumentError: method definition for Y at REPL[8]:1 has free type variables
but once you have enough type parameters, it just trusts that you didn’t do something unusual.
Although the type definition or a concrete type does not fail, instantiation does because the type’s parameter cannot be associated with the inserted TypeVar:
julia> BusVec{Int} |> dump
BusVec{Int64} <: Any
vm::Array{T, 1}
julia> BusVec{Int}([1])
ERROR: UndefVarError: `T` not defined in static parameter matching
Suggestion: run Test.detect_unbound_args to detect method arguments that do not fully constrain a type parameter.
Since Base.unwrap_unionall is already an internal function, I think getting the symbol from the internal (assume if not documented or only documented in experimental or developer documentation) .name is just as okay for working with TypeVars. I should clarify my earlier comment that it’s preferable to work with a well-maintained library that exposes public API for similar purposes because someone handles the internals for you and everyone else, and if you must dig into internals it’s better to use ones that seem designed to change less.