The documentation is absolutely correct:
julia> expand(:(println("hello $m"))) :(println((Base.string)("hello ",m)))
That is, println("hello $m") equivalent to println(string("hello", m)) . By the time the code is compiled or interpreted, it is one and the same.
However your overload
Base.string(m::MyType) = "world"
is not the correct way to overload string . This method covers only one argument of type MyType . (That's why, by the way, your code seemed to work for concatenation: this particular example involved calling string with one argument. The results would be the same if you wrote "$m" .) The right way to overload it is
Base.show(io::IO, m::MyType) = print(io, "world")
which may seem strange at first. The reason this should be overloaded is because string delegates to print , which delegates to show .
After updating the minimum working example to
type MyType x::Int end Base.show(io::IO, m::MyType) = print(io, "world") m = MyType(4) println("hello $m") println("hello " * string(m))
The result will be as expected.
As a side note, note that your example can be more efficiently written as
println("hello ", m)
This avoids the creation of intermediate lines. This illustrates why the system is configured so that string calls print , which calls show : the input / output method is more general and can print to various forms of input / output directly, whereas if it were the other way around, they should convert things to strings (requiring time allocation and therefore poor performance) before sending to IO.