I think I would try the recommended DynamicExpressions.jl first, as that looks pretty powerful and ergonomic.
But if the above solutions don’t work for you, you can also consider FunctionWrappers.jl.
julia> using FunctionWrappers: FunctionWrapper
julia> plus_f64 = FunctionWrapper{Float64, Tuple{Float64, Float64}}(+);
julia> times_f64 = FunctionWrapper{Float64, Tuple{Float64, Float64}}(*);
julia> typeof(+) == typeof(*)
false
julia> typeof(plus_f64) == typeof(times_f64)
true
julia> plus_f64(3.0, 4.0), times_f64(3.0, 4.0)
(7.0, 12.0)
FunctionWrappers lets you make the type of functions based entirely on their (concrete) input/output types, rather than the function itself. This way, you can store and call many functions in a stable way. With that, you should be able to write code to assemble and evaluate runtime expressions while avoiding eval or even dynamic dispatch.