If I understand correctly, the issue is that your closure views T as something “global” that could possibly change after its definition.
Here is one way to solve it:
julia> function f(x::AbstractArray)
T = eltype(x)
ax = axes(x)
g() = let T
zeros(T, 1000)
end
return g
end
f (generic function with 1 method)
julia> @code_warntype f(rand(3))()
MethodInstance for (::var"#g#11")()
from (::var"#g#11")() in Main at /home/guillaume/Downloads/scratchpad.jl:4
Arguments
#self#::Core.Const(var"#g#11"())
Locals
T::Union{}
Body::Union{}
1 ─ Core.NewvarNode(:(T))
│ Main.zeros(T, 1000)
└── Core.Const(:(return %2))
julia> function f(x::AbstractArray{T}) where T
ax = axes(x)
return () -> zeros(T, 1000)
end
f (generic function with 1 method)
julia> @code_warntype f(rand(3))()
MethodInstance for (::var"#1#2"{Float64})()
from (::var"#1#2"{T})() where T in Main at REPL[1]:3
Static Parameters
T = Float64
Arguments
#self#::Core.Const(var"#1#2"{Float64}())
Body::Vector{Float64}
1 ─ %1 = Main.zeros($(Expr(:static_parameter, 1)), 1000)::Vector{Float64}
└── return %1
That infers as Union{}, because it’ll throw rather than return. But replacing zeros(T, 1000) with zeros(eltype(T), 1000) does infer on at least Julia 1.7, 1.8 and master.