One rule is Scala never infers singleton type this.type. For example first consider the mechanics of it
scala> trait Foo {
     |   type T
     |   def f() = this // we left out the return type to see what Scala will infer
     | }
// defined trait Foo
scala> new Foo { type T = String }
val res0: Foo{T = String} = anon$1@6d3ad37a
scala> res0.f()
val res1: Foo = anon$1@6d3ad37a
Note how res1 has return type of Foo and not Foo { type T = String }, so we have lost some type information
scala> val x: res1.T = ""
1 |val x: res1.T = ""
  |                ^^
  |                Found:    ("" : String)
  |                Required: res1.T
Note compiler does not know res1.T is actually a String. So compiler did not infer the singleton type this.type which would have all the type information including what type member T was instantiated to
scala> trait Foo {
     |   type T
     |   def f(): this.type = this
     | }
// defined trait Foo
scala> new Foo { type T = String }
val res2: Foo{T = String} = anon$1@7d381eae
scala> res2.f()
val res3: Foo{T = String} = anon$1@7d381eae
scala> val x: res3.T = ""
val x: res3.T = ""
Note how after we explicitly declared singleton return type this.type compiler knows T is a String.
Here is another mechanical example of what happens because compiler does not infer singleton type this.type
scala> trait Foo {
     |   def f() = this // let inference do its thing
     | }
// defined trait Foo
scala> trait Bar {
     |   def g() = 42
     | }
// defined trait Bar
scala> trait Bar extends Foo {
     |   def g(): Int = 42
     | }
// defined trait Bar
scala> new Bar {}
val res5: Bar = anon$1@6a9a6a0c
scala> res5.f()
val res6: Foo = anon$1@6a9a6a0c
scala> res6.g()
1 |res6.g()
  |^^^^^^
  |value g is not a member of Foo
Note how f() call typed to Foo and not perhaps expected Bar. On the other hand if we provide explicit singleton return type this.type then
scala> trait Foo {
     |   def f(): this.type = this
     | }
// defined trait Foo
scala> trait Bar extends Foo {
     |   def g(): Int = 42
     | }
// defined trait Bar
scala> new Bar {}
val res7: Bar = anon$1@4707d60a
scala> res7.f()
val res8: Bar = anon$1@4707d60a
scala> res8.g()
val res9: Int = 42
we see f() call typed to Bar.
Those were the mechanics but what about practical applications? Two uses I am aware of are:
- supporting fluent style of programming
- supporting summoners of type class instances (see also Scala 3 summon)