News for hackers. Stuff that matters.
Re:dotNET (OK, C#) gets union types by moomin on Saturday May 23, 2026 @05:13PM (#48251592)
AFAICT, this means you won’t be able to define Either<string, string>, which is definitely a thing you sometimes want to do.
Re:dotNET (OK, C#) gets union types by sheept on Saturday May 23, 2026 @05:23PM (#48251671)
It seems like if you wrap both in a record then it should be possible:

    public record Left<T>(T Value);
    public record Right<T>(T Value);
    public union Either<L, R>(Left<L>, Right<R>);
Re:dotNET (OK, C#) gets union types by jzebedee on Saturday May 23, 2026 @05:22PM (#48251662)
C# is strongly-typed, not stringly-typed. The point of the union is to list possible outcomes as defined through their respective types.

The idiomatic way to do this would be to parse, don't validate [1] each string into a relevant type with a record or record struct. If you just wanted to return two results of the same type, you'd wrap them in a named tuple or a record that represented the actual meaning.

[1] https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...

Re:dotNET (OK, C#) gets union types by nesarkvechnep on Saturday May 23, 2026 @05:37PM (#48251797)
I guess C# is more strongly-typed than Haskell then... /s
Re:dotNET (OK, C#) gets union types by troad on Saturday May 23, 2026 @08:05PM (#48252900)
String literal typing appears to be a common feature of type systems bolted onto dynamic languages:

    # Python
    MyStringBool = Literal("Yes") | Literal("No")

    // TypeScript
    type MyStringBool = "Yes" | "No"
I assume it exists to compensate for the previous lack of typing, and consequent likelihood of ersatz typing via strings.

It would seem pretty unnecessary in Haskell, where you can just define whatever types you want without involving strings at all:

    data MyBool = Yes | No
Though of course you'd need a trivial parser, though this is probably a good idea for any string type:

    parseMyBool :: String -> MyBool 
    parseMyBool "Yes" = Yes
    parseMyBool "No" = No
    parseMyBool _ = error "..."
Interestingly, dynamic languages which make use of symbols (Ruby, Elixir, Common Lisp) probably fall closer to Haskell than Python or TS. Elixir example:

    @type my_bool() :: :yes | :no

    @spec parse_my_bool(String.t()) :: my_bool()
    def parse_my_bool("Yes"), do: :yes
    def parse_my_bool("No"), do: :no
    def parse_my_bool(_), do: throw("...")
Re:dotNET (OK, C#) gets union types by nesarkvechnep on Sunday May 24, 2026 @01:20AM (#48254630)
Thank you for the lecture but I didn’t mean that. I meant `Either String String` is possible in Haskell and not in C# because… C# is strongly-typed.
Re:dotNET (OK, C#) gets union types by troad on Sunday May 24, 2026 @02:04AM (#48254841)
You're welcome! Knowing is half the battle.

That Haskell snippet is just syntax sugar for Left(string) | Right(string), which is trivial in any language with unions.

Not clear why it would be an improvement over just naming the alternatives something meaningful, but if you're wedded to Left and Right, go for it.

Re:dotNET (OK, C#) gets union types by jaen on Sunday May 24, 2026 @03:54AM (#48255399)
String literals are structural types which are way more expressive than regular (Haskell) ADTs, which are nominal types.

In TS in particular, in combination with other features (mapped types), they are equivalent to row polymorphism + whatever Haskell/GHC features enable type families to specialize on constant literal arguments (or you can use atomic types, but that's not structural / open-world)... so pretty advanced.

This is valid TS/Python:

    type ABC = "A" |"B" | "C"
    type AB = "A" | "B"
    const x: AB = "A";
    const y: ABC = x;
The equivalent Haskell requires using several extensions.
Re:dotNET (OK, C#) gets union types by troad on Sunday May 24, 2026 @04:40AM (#48255636)
I know. I literally gave the example of a Python Literal in the post you're replying to. TS too. :)

My overall point is that Haskell's type system is sufficiently expressive (you may not have "A" | "B" | "C", but you do have A | B | C) that there's no obvious remaining use case for string literals, unless you're thinking of typing input by way of expected literals instead of actually parsing it, which is... a choice. :P

Re:dotNET (OK, C#) gets union types by jaen on Sunday May 24, 2026 @05:57AM (#48255974)
By Haskell's type system do you mean with all the GHC extensions?

Because TypeScript has structural sub-typing, while standard Haskell (eg. `A | B | C`) has neither subtyping nor structural typing, which both are very useful features for safe "integration/glue" type of programs.

(String) literals form a fundamental part of the TS "row polymorphism" (record types) and eg. tuple union type implementation.

You can type a non-empty array that starts with zero...

    type Arr = [0, ...number[]];
    const a: Arr = [0, 1, 2, 3, 4]
Now try in Haskell.
Re:dotNET (OK, C#) gets union types by troad on Sunday May 24, 2026 @08:05AM (#48256590)
> By Haskell's type system do you mean with all the GHC extensions?

No? What extensions does `A | B | C` require?

> Haskell has neither subtyping nor structural typing

Is subtyping back in? Good news for Java and C++.

Re structural typing, I would ask what behaviour you're after, specifically. For example, this is a valid, typed Haskell function for any two values that can be added, including any user-defined ones:

    adder a b = a + b
If by structural typing you mean silently coercing types that the compiler deems structurally equivalent, then no, but I don't think many people writing Haskell would consider that desirable. A `Person` may have an age (40) and a `Wine` may have an age (2005), but you're not going to get sensible results if you start adding those two together, and your compiler should probably stop you.

Structural typing is the sort of thing that is very valuable if you're bolting a type system onto a language with a cornucopia of untyped structs, like JS objects. It is comparatively much less valuable if you're working in a typed ecosystem to begin with, since you're not liable to have loose untyped structs floating around that require coercion.

> "integration/glue" type of programs

It does sound a lot like you're using string literals in lieu of parsing foreign input, which strikes me as a pretty bad idea. Particularly in a language like TS, which is not type safe at runtime, and which will happily ingest an unexpected value, silently coerce it in all sorts of fun and wacky ways, and cause behaviour far removed from what any static analysis of the TS source would suggest.

> You can type a non-empty array that starts with zero

Can you please name me any possible actual use for this? Especially given the type doesn't even exist at runtime and will never be enforced on input data, so this is a once-off check for comptime constants?

Re:dotNET (OK, C#) gets union types by jaen on Sunday May 24, 2026 @08:38AM (#48256787)
OCaml and Scala, both also famously strongly typed functional languages, also have structural typing (OCaml even has many different kinds at many different levels!). Mainstream, Go is based on structural (interface) typing.

The Person/Wine example is a pointless strawman. That's not what structural typing is generally used for.

The entire comment is basically making up strawmans... I didn't give practical examples to save space, obviously, it was just to disambiguate what I meant.

TypeScript has several runtime-safe advanced validators based on its type system (most well-known being Zod), capable of enforcing types similar to what I provided.

To conclude, these type system features were added by multiple experienced language designers for a reason, to languages that already had functional ADTs, so going "huh but what are these even useful for?!" to me sounds a bit clueless (or argumentative), so I don't see a productive continuation to this discussion.

Re:dotNET (OK, C#) gets union types by troad on Sunday May 24, 2026 @08:52AM (#48256887)
Haha, I knew you'd bring up Go. I even considered pre-emptively dropping Rust and Zig as counterexamples. I think most people who favour static typing consider the duck-typed interfaces of Go to be a mistake. Personally, I consider all of Go to be a mistake.

> these type system features were added by multiple experienced language designers for a reason

Oooh, an appeal to authority, where that authority isn't even named. I'll have you know a famous queen told me you're wrong on this, also for "a reason".

> TypeScript has several runtime-safe advanced validators based on its type system (most well-known being Zod), capable of enforcing types similar to what I provided.

Right, so TS typing is so amazing it requires runtime parser libraries from NPM, and Haskell is less sophisticated because it's not stringly typed.

You realise the entire, complete, exhaustive runtime schema for your zero-first non-empty integer array example looks like this in Haskell, right?

    sch (0:_) = True
    sch _ = False
That's a complete function that somehow manages to work without pulling in NPM dependencies. The best JavaScript minds of our generation remain baffled.

> The Person/Wine example is a pointless strawman

> I didn't give practical examples to save space, obviously, it was just to disambiguate what I meant.

So when you use illustrative examples, it is to "disambiguate what you meant" (huh?), and when I do it, they're "pointless strawmen". A little hypocritical, no?

> a bit ignorant

Honestly, I don't think you know what you're talking about at all. You clearly hadn't even read my comment when you started replying with Python and TS examples... that were already in my comment.

It also really sounds like you're using string literals to type input without properly parsing it, which is just a terrible idea. Haskell's type system is designed precisely to protect you from this sort of mistake. [0] No, you're not always going to get what you expect. No, your JS program will never let you know that's the case. No, a sane type system does not require mainlining runtime parser libraries from the biohazardous oceans of NPM. A schema in Haskell is going to be significantly shorter and sounder than anything in Zod, and you don't need a library for it.

As I said above, TS' type system makes sense for a type system bolted onto a dynamic language post facto. TS needs to more tightly link (even mildly conflate) values and types, since it needs to do a lot of clever narrowing to figure out what mad ball of JS it is dealing with at any given time. Haskell does not operate under any such constraint.

> I don't see a productive continuation to this discussion.

Phew. Timesaver.

Of course the irony of all this is that I use TS daily, and Haskell quite rarely.

[0] https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...

Re:dotNET (OK, C#) gets union types by jaen on Sunday May 24, 2026 @10:08AM (#48257400)
Yeah, you're just continuing to take whatever was written argumentatively/maliciously as predicted.

Does not seem like this:

> Please respond to the strongest plausible interpretation of what someone says, not a weaker one that's easier to criticize. Assume good faith.

> Personally, I consider all of Go to be a mistake.

You also consider Scala and OCaml to be mistakes? Because all of what I've mentioned also works in a very similar way in Scala.

> an appeal to authority

No, I didn't appeal to authority. It's the opposite - it's statistics. Multiple experienced language designers don't add features later for nothing. For a similar process, see eg. closures getting added to every mainstream language.

> complete, exhaustive runtime schema

Again, I wasn't talking about runtime schemas, but types. I only mentioned runtime as a counterpoint to the false statement that TypeScript doesn't enforce this. Only reducing this to runtime checking is a fallacy, again.

> A little hypocritical, no?

No, they aren't comparable since I wasn't using the examples as supports for an argument of whether X is better than Y. Strawmen involve argumentation.

> read my comment... examples... that were already in my comment

I read it in detail, the problem is you didn't really read my comment in detail, which illustrated both the subtyping and structural typing aspects (albeit trivially, yes), which yours didn't.

> You're also fairly clearly using string literals to type input without properly parsing it

Okay then, the arrogance of this is pretty astounding... You seem to know what I'm doing better than I am!

To be clear, I'm not doing any of that. And I've written Haskell way before I wrote any TypeScript.

Re:dotNET (OK, C#) gets union types by troad on Sunday May 24, 2026 @10:43AM (#48257660)
I believe I was promised a respite from this.

Everything I said to you prior to my last message was fairly gentle. I'm not sure what response you were expecting to what you wrote at that point, which was to accuse me of disingenuous strawman arguments and ignorance. Perhaps you yourself would have benefited from a rule refresher?

> You seem to know what I'm doing better than I am

Apparently so! Trust me, it gives me no pleasure, and I'd rather I didn't.

> Again, I wasn't talking about runtime schemas, but types. I only mentioned runtime as a counterpoint to the false statement that TypeScript doesn't enforce this. Only reducing this to runtime checking is a fallacy, again.

My dear friend, this is almost completely incoherent.

I wish to strictly type myself as a function at this point, whereby your messages are my input, and void is my output. Zod, activate!

Re:dotNET (OK, C#) gets union types by nvlled on Sunday May 24, 2026 @11:14AM (#48257911)
I would say, with a striped-shirt and not a glove in my hand, that the person you were fighting with gets the score, and wins by technicality. I suggest you take the bench first and review your comments and reflect with a wet towel on your head.
Re:dotNET (OK, C#) gets union types by troad on Sunday May 24, 2026 @10:43PM (#48262958)
Having reviewed as instructed, I stand by everything I said here. Let's review the action replay.

I began by noting that TS has literals and set theoretic types, and that this makes sense for a post-facto type system bolted on top of a dynamic language. Jaen showed up to inform me that TS has literals and set theoretic types, implying he hadn't read my post.

I noted that typed string literals are not generally considered desirable within strictly typed environments. "Parse, don't validate", etc. Jaen seemed to follow this up by aggressively trying to prove that TS is somehow "better" than Haskell. His arguments comprised the fact that TS has literals and set theoretic types (again, yes, this was is in my initial post), and a mixture of personal insults and just straight up nonsense (I wasn't the one to bring Zod into a discussion of type systems... ). At that point I did have a little fun with things, since it had become clear Jaen was not a constructive or good faith interlocutor.

Jaen's central misapprehensions seem to be that (a) I don't understand literals and set theoretic types, despite this whole thread being in reply to a post where I give examples of them in TS; and (b) that I care which type system is "better".

As I repeated a number of times above, TS' type system makes sense for a type system bolted onto a dynamic language. It's extremely useful when the underlying language has oodles of untyped structs flying every which way. Conversely, Haskell's type system makes sense within a holistic strictly typed environment. Structural typing would be a gaping hole in Haskell's strict type safety, which is kind of Haskell's whole thing. Neither system is better, each has its use. Different strokes for different folks.

I don't usually put much stock in upvotes, but I do note I seem to have the edge there. Seems that our esteemed panel of armchair referees respectfully dissent from your narrative, nvlled. :)

Re:dotNET (OK, C#) gets union types by jaen on Monday May 25, 2026 @01:35PM (#48269456)
You seem to be aggressively misreading what I said, stuck in your own "world" and instead of asking for clarifying questions, making unfounded assumptions.

> I began by noting that TS has literals and set theoretic types

Can you please quote me exactly the part of your original comment that implies that literal types are structural and have subtyping? Because that is what I said. If you did imply that, well, excuse me for helping you by clarifying then.

> aggressively trying to prove that TS is somehow "better" than Haskell

Sorry, what? Refer to the first line of this comment. Just saying that some type system features are extensions or harder to use in Haskell says nothing about the superiority of TS. This is completely your imagination.

Here's me criticizing TS 4 months ago: https://news.ycombinator.com/item?id=46501061

> I wasn't the one to bring Zod into a discussion of type systems...

Not sure why this is even relevant, but this was a direct reply to: "...language like TS, which is not type safe at runtime, and which will happily ingest an unexpected value, silently coerce it in all sorts of fun and wacky ways"

Zod (just a random library) is a direct counter-example to that. There may be better counter-examples. One just needs a proof of existence - it doesn't have to be good.

I then had to repeat that I am also talking about static types because of: "...exhaustive runtime schema for your zero-first non-empty integer array example...". TS can both enforce that as a type (which you never presented for Haskell) and as a value. (nominally typed solutions quickly run into ergonomic problems like phantom types not being composable across library boundaries - eg. refinement types, which are even safer by nature, are structural!)

In any case, I'd estimate 99% of people using TS don't encounter any type safety issues caused by the design of TS. Haskell has `unsafeCoerce` too, just a bit wordier than in TS.

If wanting to talk about real unsoundness, one would mention something like bivariance (see also: linked comment), but even then almost all of that is entirely irrelevant in most practical software engineering.

> (a) I don't understand literals and set theoretic types, despite this whole thread being in reply to a post where I give examples of them in TS; and (b) that I care which type system is "better".

Huh, what? Again, I'm thinking none of that. Again, you imply you know better what I'm thinking...

> Structural typing would be a gaping hole in Haskell's strict type safety

I mean, yeah, let me just repeat OCaml and Scala here, both also famously type safe languages... Why make an argument when there's two immediate counterexamples that were already mentioned?

> but I do note I seem to have the edge there

This pretty much sums up the difference in attitude, I'm not here to score internet points in an argument.

I just wanted to comment on why the world is not black and white and even technically flawed languages like TS have something to learn from.

Re:dotNET (OK, C#) gets union types by troad on Monday May 25, 2026 @08:00PM (#48273300)
*bangs on Zod*

void

Re:dotNET (OK, C#) gets union types by goto11 on Saturday May 23, 2026 @07:29PM (#48252647)
You cant have a `type Foo = String | Strimg` in Haskell either.
Re:dotNET (OK, C#) gets union types by bertylicious on Saturday May 23, 2026 @11:12PM (#48253982)
But you can have an `Either String String` which is what GP was talking about.
Re:dotNET (OK, C#) gets union types by bertylicious on Sunday May 24, 2026 @02:54AM (#48255092)
My mistake. I see my oversight now. `Either String String` is not equivalent to `String | String`, but to `Left String | Right String`. The same must be done for the C# version.
Re:dotNET (OK, C#) gets union types by goto11 on Sunday May 24, 2026 @03:35AM (#48255296)
Yes, you must have individual constructors for the left and right cases in order to distinguish them. In C# you would use two distinct record types for this. Haskell’s syntax is more concise though, since you define the constructors inline in the declaration of the sum type.
Re:dotNET (OK, C#) gets union types by goto11 on Saturday May 23, 2026 @07:14PM (#48252541)
Well it is a type union. The union of string and string is just string.
Re:dotNET (OK, C#) gets union types by bertylicious on Saturday May 23, 2026 @11:10PM (#48253972)
No, it's a union of a left value (that happens to be a string) and a right value (that happens to be a string). But the compiler-generated code can't tell them apart.
Re:dotNET (OK, C#) gets union types by jafffsuds on Sunday May 24, 2026 @12:11AM (#48254295)
What you are describing is something different called a disjoint union which will maintain the identities of the left and right values when there is overlap.

The C# unions appear to behave like unions, not disjoint unions.

Re:dotNET (OK, C#) gets union types by bertylicious on Sunday May 24, 2026 @03:05AM (#48255167)
My mistake. I see my oversight now. `Either String String` is not equivalent to `String | String`, but to `Left String | Right String`. The same must be done for the C# version.

You are correct that this requires support for disjoint unions (aka tagged unions), which Haskell always had and C# will soon have.

Re:dotNET (OK, C#) gets union types by throw1234567891 on Saturday May 23, 2026 @05:20PM (#48251651)
but can you define T1 and T2 of string, then use Either<T1, T2>?
Re:dotNET (OK, C#) gets union types by eterm on Saturday May 23, 2026 @06:03PM (#48252001)
Could you be clearer about what you mean, since string is a sealed type in C#, so what exactly do you mean T1 and T2 of string?
Re:dotNET (OK, C#) gets union types by rawling on Saturday May 23, 2026 @06:16PM (#48252120)
A record wrapping a string, indicating what the string represents, so you can't mix it up with a different thing also represented by a string.
Re:dotNET (OK, C#) gets union types by eterm on Saturday May 23, 2026 @06:28PM (#48252213)
Yes, you can have two different record types which both wrap a string value.

As a (bad) trivial example, you could wrap reading a file in this kind of monstrosity:

    var fileResult = Helpers.ReadFile(@"c:\temp\test.txt");

    Console.WriteLine("Extracted:");
    Console.WriteLine(Helpers.ExtractString(fileResult));

    public record FileRead(string value);
    public record FileError(string value);
    public union FileResult(FileError, FileRead);

    public static class Helpers
    {
        public static FileResult ReadFile(string fileName)
        {
            try
            {
                var fileResult = System.IO.File.ReadAllText(fileName);
                return new FileRead(fileResult);
            }
            catch (Exception ex)
            {
                return new FileError(ex.Message);
            }
        }

        public static string ExtractString(FileResult result)
        {
            return result switch
            {
                FileError err => $"An Error occured: {err.value}",
                FileRead content => content.value,
                _ => throw new NotImplementedException()
            };
        }
    }

Now, such an example would be an odd way to do things ( particuarly because we're not actually avoiding the try/catch inside ), but you get the point. Both FileRead(string value) and FileError(string value) wrap strings in the same way, but are different record types, and the union FileResult ties them back together in a way where you can tell which you have.

It's more useful implemented a level deeper, so that the exception is never raised and caught, because exceptions aren't particularly cheap in .NET.

Re:dotNET (OK, C#) gets union types by throw1234567891 on Wednesday May 27, 2026 @04:23AM (#48291309)

    type T1 string
    type T2 string
    type Meh Either<T1, T2>
Re:dotNET (OK, C#) gets union types by twodave on Saturday May 23, 2026 @11:33PM (#48254104)
You can use implicit operators or a library like Vogen to accomplish the same thing in a way that they can be coerced as strings. This isn’t a real issue.
Re:dotNET (OK, C#) gets union types by Metasyntactic on Sunday May 24, 2026 @04:51AM (#48255684)
Hi there. One of the C# lang designers here.

You're correct. The unions we're working on right now are 'type unions'. So the type is inherent in the union distinction, and you would not be able to distinguish that case. That said, we're also looking at full blown discriminated unions (you can look at one of my proposals for that here: https://github.com/dotnet/csharplang/blob/main/meetings/work...), which would allow for that. Syntax entirely tbd, but you'd do something like:

  enum struct Either<T1, T2> // or enum class
  {
     First(T1 value),
     Second(T2 value)
  }
We view these features as complimentary. Indeed, if you look at the extended enum proposal, you'll see it builds on top of unions and closed types (another proposal coming in the next version of the lang).
Re:dotNET (OK, C#) gets union types by moomin on Monday May 25, 2026 @04:48PM (#48271500)
Thank you! Even what’s there is going to be really useful to me but I’ll continue to look forward to a full-fat implementation.