I don’t think you can generally get more specific than Function by default, because the output type and implementation of a function can depend on its input values. However, you can use callable structs to essentially append metadata to a function.
Example:
struct FunctionWrapper
input_types::Vector{DataType}
output_type::DataType
f::Function
end
# Callable struct syntax
function (fw::FunctionWrapper)(args...)
@assert all(isa.(args, fw.input_types))
output = fw.f(args...)
@assert output isa fw.output_type
return output
end
foo(x::Int) = x + 1
foo2 = FunctionWrapper([Int], Int, foo)
julia> foo2(5)
6
julia> foo2(5.0)
ERROR: AssertionError: all(isa.(args, fw.input_types))
This is obviously a lot of extra code so you would only do it in a few cases where you really needed that extra metadata.