I seem to be doing a very poor job of explaining my idea. I’m trying to describe a world where Option[T] is the only type that you will see in Scala’s type system, and something like T | null would only exist as a runtime representation.
In the case where you have a runtime representation of Option[String | Null], the type system would see it as Option[Option[String]], and wouldn’t type check against String | null, and there for the conversion code I showed above wouldn’t be used. That specific code only runs when you are trying to write (either as a method argument or setting a variable) to a T | null. This is why I tried to emphasize the importance of tracking both the type-system type, and the runtime representation.
Some(None) is still distinguishable from None, because T | null is only equivalent to Option[T]. If you have a runtime representation of Option[T] | null, it still maps to Option[Option[T]] in the type system. If you try to write a runtime value represented as Option[T] | null to a runtime representation of say Option[T | null], it’s going to know that it needs an option, so it would convert null to None, and Some(t) to Some(Some(t)).
I hope that makes sense. Maybe I need some better examples. But the overall idea is that the Scala compiler uses Option[T] as its only type-level representation, but it can support different runtime representations. However, those runtime representations are meant to only exist at the edges of interop. E.g., if you subclass a Java class that has a field foo that can be a String or null, then even though it looks like foo: Option[String] to the type system, the compiler knows to keep the runtime representation as String | null. But when you read from that field, it will convert it to Option[T], and when you write to it, it will convert the Option[T] back to T | null. The conditional conversion would come into play when you don’t know the runtime representation of the value.
The only edge cases I can really think of are when you somehow just don’t know (maybe because of type erasure?) either the runtime representation of the destination or the source. But I’m not entirely sure when you could run into that problem.