I might be wildly wrong about this.
I feel like extending unions and records behave more like binary operations.
For example, User a : { username: Str, email: Str }a could be User a : { username: Str, email: Str } & a or even User a : { username: Str } & { email: Str } & a. But if I had access to this feature, I would write User : { username: Str, email: Str } and then use User & a when I want to allow a user with extra fields, so I wouldn't need two different type annotations for User and User a.
Unions could be [PageNotFound, Timeout, BadPayload Str] | a or [PageNotFound] | [Timeout] | [BadPayload Str] | a instead of [PageNotFound, Timeout, BadPayload Str]a.
In the example in roc-for-elm-programmers, I imagine the resulting type of doStuff like Filename -> Task File.Data (File.WriteErr | File.ReadErr | Http.Err). But then the rest of the example explains why we need to change the definition of File.WriteErr, etc. including a type variable in the definition and in every single usage, in order to acheive this. I also imagine that the compiler's error message could be better if it outputs File.WriteErr | File.ReadErr | Http.Err instead of File.WriteErr (File.ReadErr (Http.Err []).
Extending tag unions and records indeed are binary operations. But, they are right-associative, rather than being arbitrarily composable. In your last example, File.WriteErr | File.ReadErr | Http.Err and File.WriteErr (File.ReadErr (Http.Err []) are isomorphic - the latter is basically File.WriteErr | (File.ReadErr | (Http.Err | [])).
It sounds like maybe what you're trying to get it, is that the ergonomics of these types could be improved if they were instead compared by subtyping. Is that correct?
Yes I guess my first impression is that it's a bit counter-intuitive but you're right, it's still a binary operation.
Last updated: Jun 16 2026 at 16:19 UTC