Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
TypeScript: Control flow analysis for destructured discriminated unions (github.com/microsoft)
299 points by tosh on Nov 3, 2021 | hide | past | favorite | 107 comments


It's encouraging seeing all the work going into making tagged unions a natural part of TS. I've tried to introduce them multiple times, and have been shot down by my team on every attempt. I think they still come across as "hacky," and more JS-minded people who are totally cool validating with if-else everywhere still can't appreciate the benefits of annotating data in this manner. I hope TS eventually arrives at a standard, low impact way to create and manipulate such structures; maybe more people will be on board then. It looks like we're moving in the right direction.


Well, tagged unions, like many TS features, are designed to enable type-checking on vaguely idiomatic JS code, aren't they? That checks properties of objects in if-else statements? It sucks that people aren't open to defining their structures in a way that aids type-checking, if that's what you mean!


I understand that familiarity is TS' selling point, but that dreadful I-word was actually used against me when trying to introduce these unions. For whatever reason, some people don't find the syntax very natural at the moment. With no standard convention around the tag's name (it is `kind` in the linked PR, but I've seen it specified in many other ways), my suggestion came across like a cute self-invented gimmick rather than a fundamental concept in programming. Others had trouble differentiating the tag's literal type from a vanilla string due to the quotes.

If some of the noise were eventually removed and tagged unions became true first class citizens in TS' syntax, I think some of these colleagues without as much typing experience might be more open to the idea—or at the very least understand it in the first place.


> my suggestion came across like a cute self-invented gimmick rather than a fundamental concept in programming

Maybe the official handbook could help convince them this isn't the case: https://www.typescriptlang.org/docs/handbook/2/narrowing.htm...


I love discriminated unions, but bounced off the typescript approach because it looked like too much of a pain to actually be worth it. Hopefully features like this can make it worthwhile, because I really miss them.


Tagged unions still require rather cumbersome JavaScript syntax to use. While the type inference support is great, much of the benefit is IMHO lost because of the syntax.


Very nice!

The very, very related thing I found myself wishing I had just today is the ability to destructure discriminated union fields which only exist on one component type of the union.

As an example, with the following type system:

    type Output = OutputExists | OutputDoesntExist;
    interface OutputExists { exists: true, url: string };
    interface OutputDoesntExist { exists: false };
The ability to destructure like:

    const f = ({ exists, url }: Output) => {}
Such that, at the time of destructuring:

    exists: true | false
    url: string | undefined
I believe, with this PR, if we re-structured the types above to:

    interface OutputDoesntExist { exists: false, url: undefined };
That destructuring would be possible, with all the control flow analysis necessary to make it useful. Today, even that wouldn't be possible, so definitely excited for this change.

But, I'm curious if anyone knows if this change can extend to situations where a field isn't even defined in one of the unions, and the compiler can just insert an undefined type for those fields. Or, maybe this would have some unresolveable side-effects I'm not considering.


What I do:

    interface OutputDoesntExist {
      exists: false;
      url?: never;
    }
Now it’s:

- Optional in the union type

- Always present and has the correct type in the exists: true case

- Still optional in the exists: false case, but if you try to access it you always get a type error


"never" is not really ever a good choice for a property/member type, because it is assignable to all types. A function that literally never returns could/should have a return type of never, for example, but if the function actually returns a value, you have a "value of type never" which is supposed be impossible, and never acts like any (because a contradiction implies anything).


> "never" is not really ever a good choice for a property/member type, because it is assignable to all types.

This isn’t right. It’s not assignable to anything other than never, that’s why never is so useful for this use case.


I like the use of 'never' over just 'undefined'; really drives home that not only is it not there, but it shouldn't exist at all in that case.

I'm trying to think of a reason why the compiler couldn't allow that destructuring, implicitly type the values missing in some components of the union as 'T | ... | never', then lean on the control flow analysis it looks like we're getting in 4.6 to specify the type as the discriminated union is de-discriminated (lol). JS itself does allow the destructuring of elements missing from an object, they come out as undefined, so I don't believe there'd be any underlying issues.

Hopefully we see it in 4.7!


This is the second release in a row with improved control flow narrowing like this. If they’re not actively working on it I’d be surprised. Just a matter of physics (time).

The only underlying issue I could foresee is async. Eg narrow, await, infer. But I’d assume that affects all similar cases.

IMO this is as good a time as any to introduce an immutable-by-default mode. If you know it won’t change you can trust the previously narrowed type at any async boundary.


Don’t think they can do anything that isn’t strictly a superset of JS. That said I think you can use `const` and `Readonly<T>` to get some safety against mutability.


Immutable by default can still be a strict superset of JS. It just requires treating const assignments and function parameters as Readonly (or ReadonlyArray/ReadonlyMap/ReadonlySet) and introducing a Mutable type/mutable keyword.

In my own personal projects I make nearly everything immutable at the type level and, while it makes my code safer it’s an enormous pain in the butt to type readonly on every property. As a matter of habit it has one other benefit though: you never have to worry about accepting `as const` values in function calls.


I don't think the language designers would consider this, because there is a difference in TypeScript between a property existing with type undefined and not existing, regardless of the fact that it might sometimes be convenient if a non-existent property acted like a property that existed and had type undefined.


Really? I didn't know that. That means, there is a third kind of null?


var x = {a: 1, b: undefined};

x.b and x.c are both undefined, but `'b' in x` is true, while `'c' in x` is false.

You can check what keys have been provided, and... well, you could use that to determine things about your data.


This difference is illustrated by the types : { a: string, b : string | undefined } and { a: string, b?: string }


The underlying object that the runtime creates in memory can have a property or not. If it has a property, that property can have no value (undefined). It can also be null, which is a value.

So, you end up with two different runtime memory representations that when you query an object's property can both result in an "undefined". Cause of this is Javascript's laissez-faire approach to type safety. The implicit conversions (e.g. string+number+anyType works) are one, and not being strict about objects and their properties is another. In a strict language accessing a non-existent property would be an error and not even make it through the compiler.

Somewhat unrelated to the question I responded to:

Typescript is not a different language at all, it really only adds type annotations and the actual language is 100% ECMAscript apart from namespaces (no longer necessary, they are from pre-ES2015 times when JS was lacking features) or enums and maybe some minor class helper stuff nothing at all needs to be transpiled to get from Typescript to ECMAScript. You just need to remove the type annotations. That is why TS ends up with still having issues because it cannot deny the easy-going attitude of the code.

Typescript is not the best architecture: It is a monolith software that supports completely different things in one giant piece of software. We used to have compiler, linker, profiler, code analysis tools, all separate tools.

Typescript in one piece bundles type checking and transpiling in one, and on top of that a quite elaborate and extensive editor support (language service for editors to use). For no good reason, you can not use the transpiling feature at all and only use TS with an IDE to give type advice, and then use Babel to remove the Typescript annotations. Transpiling is really from Javascript to Javascript - working around newer ECMAScript features that a targeted older runtime does not have.

I think Microsoft did a great disservice to their own product by crating this monolith, which unfortunately also created the impression that Typescript is a different language.

If they had it split, and/or made that split clear, that type annotations are just added on top of whatever the latest ECMAScript is, and that transpiling is separate and does not work on a whole new language, and is only necessary when targeting platforms that don't support newer ECMAscript features used in the code, there might be less confusion about what TS is. Flow, the other type system, always made it clear it's a type-annotation add-on. TS is actually not different, they just chose to also bundle the transpiler.

This monolith also makes TS a lot less flexible: If you only had type annotations and checking and transpiling separate you could have more flexibility. You can extend Babel with your own modules easily, e.g. to add a module that removes debug code in a production build. With TS you are limited to using exactly the features they implemented. This is less about the type stuff, more about the environment. For example, I have code that keeps different platform versions at once, and in the build only one is used. I have no way of telling TypeScript that. If the tool was split and as flexible as Babel it would be easy to write a tiny module or plugin.


> For no good reason, you can not use the transpiling feature at all and only use TS with an IDE to give type advice, and then use Babel to remove the Typescript annotations.

Babel has had extensions (including semi-official ones supported by the Typescript team at this point) supporting the removal of Typescript type annotations and there are many projects in the wild today that use Babel entirely for transpilation and Typescript only for language services (IDE).

Relatedly, a bunch of linting that Typescript did but was more generally useful has moved to upstream ESLint, and upstream ESLint also more directly supports Typescript type annotation removal.


I'm not sure why you are telling me, I of course know, it's what I'm using. I talked about it in my post. I think you misunderstood my post.


I repeatedly come across the same issue. So I gave it a try running TS 4.6 Nightly [0]

In short:

    interface OutputDoesntExist { exists: false };
    // error, as ever :(

0 - https://www.typescriptlang.org/play?ts=4.6.0-dev.20211103#co...


Is there another example than the one you provided?

What I see in OutputExists/OutputNotExists is actually a Maybe/Option type.

    doSomething<Maybe<Output>>(someObj)
      .map(({ url }) => url)
      .getOr(null)

   // value could either be url or null


Yeah my example was really just an Option type.

A better example: think of Kubernetes API objects. In short, expressed as typescript interfaces:

    type KubeObject = Pod | Service;
    interface Pod { kind: "Pod", metadata: Metadata, spec: { containers: Container[] } };
    interface Service { kind: "Service", metadata: Metadata, spec: { selector: ServiceSelector } };

    const f = ({ kind, spec: { containers, selector } }: KubeObject) => {}
So, the ability to destructure both 'spec.containers' and 'spec.selector' given just a KubeObject, and have the compiler use control flow analysis on 'kind' to mark one as 'never' or 'undefined' when its destructured on a type where it doesn't make sense.


One thing I'm wondering: with all the bells and whistles and insanely complex things TS now supports, why does it still fail to catch seemingly simple mistakes?

For instance, it's sooo easy to write a React component taking in another component as a prop and some props for that other component, to rendering it. But somehow lose the type safety of the props being passed in. Like, you write some generics and the compiler stops complaining inside your component. But if you go back out and then try to pass some weird props instead, it doesn't care. I feel it's too easy to write code that looks tyoe safe but subtly isn't.


I'd say it's two things:

1. Typescript has to work with Javascript, which inherently makes things difficult and you have to choose between correctness or ergonomics. TS went for the latter, which probably was a good decision, otherwise no one would use it now.

2. While I have a lot of respect for the creators of TS, the language is not very consistent and uses a lot of "special cases" or "special features" to solve specific problems. But it also means that it becomes hard to ensure that all these features work together well and in the way that users expect. Other languages lack features that TS offer, but they feel more "consistent" or reliable.


Just a side note / counter note:

>"While I have a lot of respect for the creators of TS,"

Creators of TypeScript are Microsoft.

To any readers unfamiliar with TypeScript and Microsoft's ethics,

be sure to research their past corporate ethics to understand whether or not they are trustworthy as a company (or at least those who lead their overarching market strategies.

Here is one example, and to me, it describes where TypeScript is going (imho):

"The strategy's three phases are:

---> Embrace: Development of software substantially compatible with a competing product, or implementing a public standard.

---> Extend: Addition and promotion of features not supported by the competing product or part of the standard, creating interoperability problems for customers who try to use the "simple" standard.

---> Extinguish: When extensions become a de facto standard because of their dominant market share, they marginalize competitors that do not or cannot support the new extensions."

https://en.wikipedia.org/wiki/Embrace,_extend,_and_extinguis...

For this reason, out of sheer convictions in anti-unethical corporate behavior, I steer clear of TypeScript, and I encourage others to do so as well-- especially those who care about corporate ethics.


What I meant is, I respect the technical competency of the people who designed TS the programing language.

I agree that Microsoft's influence should be observed and considered carefully.


> [TypeScript] is not very consistent and uses a lot of "special cases" or "special features" to solve specific problems.

That's very interesting. Could you please provide an example that showcases that inconsistency?


This very post is a good example. Typescript has destructuring and control-flow analysis/refinement. But as you can see from the issues that are fixed by this PR, these two things haven't worked together very well.

This comes from trying to fix 1) compiler errors in conditions where the code is safe but the compiler can't understand that and 2) destructuring products/co-products.

Even if both features work on their own and help in certain cases, combining them leads to undesired results.


>as you can see from the issues that are fixed by this PR,

should there be a link to a PR in there somewhere?


The PR is the article submitted in this thread, https://github.com/microsoft/TypeScript/pull/46266


ah ok, thanks. The phrase 'this PR' confused me.


Because Typescript only exists on the compiler, while the browser only undestands JavaScript, it is the usual problem of guest languages, the platform leaks.


Well, you could, if people really wanted to, have some sort of runtime type safety, but doesn't seem like that's what people want for various reasons, one thing could be the possible performance impact. Instead, TypeScript only offers type safety at development time.

It seems matsemann fundamentally missed the idea of TypeScript, as it's very clear it only offers compile-time checks and nothing for the runtime.


Ideally one day TypeScript would take JavaScript's place, but no browser vendor seems keen on the idea.

On the other hand, TypeScript success in adoption is based on following another proven attempts, C++ and Objective-C.

With such approaches, the warts of the copy-paste compatibility with the underlying platform are both a sin and a curse.


> Ideally one day TypeScript would take JavaScript's place, but no browser vendor seems keen on the idea.

Oh god I hope not. JavaScript is hopefully here to stay, and will remain the runtime of the web together with WASM.

TypeScript is a neat idea for enterprise companies where engineering standards tend to not be that great, but that doesn't mean we should lower the possibilities of the current runtime to the lowest common denominator.


It would still be possible to write JavaScript like code, that is Typescript selling point vs CoffeeScript and Dart, so I don't see a problem with it.


My point is that I can change what I pass in in my code, and the compile time check doesn't catch it. I haven't "fundamentally missed" anything, it's you misreading my post, thanks.


  > it's sooo easy to write a React component taking in another component as a prop and some props for that other component ... but somehow lose the type safety of the props being passed in.
Previous comments interpreted this, I think, as "why don't I have type safety at runtime." Obviously, it's really little more than a very powerful static analyser on top of a superset of JavaScript. I'm not certain you were asking that; without a little more specifics to what you were trying to do or a code example, I'm not positive I'm understanding the issue you're describing correctly, but I think you're saying that in certain cases, with a React component where another React component is being passed as a parameter, along with other props, you can still reference the root component somewhere else with the wrong parameters and TypeScript doesn't complain.

Personally speaking, I have never written React without TypeScript in the last 3 years and have spent about half of my work time in that space. If you haven't taken a recent look and tried the scenario you describe in the latest version of the language, I'd suggest starting there. I remember, early on -- way prior to Hooks and the like -- we were looking at various tooling options around state management (react-redux/mobx/others) to use for a large project and ended up not choosing react-redux due to it being impossible/almost-impossible to get type safety in some cases[0].

Ninety-nine percent of the time when `tsc` fails to produce an error/indicate something useful to you in your IDE of choice like this, it's because implicit typing couldn't figure something important out and the project's configuration was set lax enough to quietly ignore it. One easy fix for me was to go full `strict`; disallow `any`. When typing can't be determined implicitly, you get a shiny error. This leads to the second thing that becomes necessary: providing types explicitly for functions/others -- in certain cases, sometimes because it doesn't work without it, sometimes just to have it clearly defined/obvious. Type aliases help a lot for drying things up and keeping the "type noise" to a minimum.

It's also possible that whatever tooling you're using to package/build things is interfering in some way (or publishing the result of the compile regardless of errors/warnings).

The strictest settings will bring a lot of new errors, and you'll probably be adding a lot of type info and adjusting code accordingly if you enable these in a project that was more lax, however, I've found it's usually worth it. The issues tend to also bring visibility to a really subtle bug or ... fifteen. Run the latest versions of TS -- improvements around this come constantly. And these days, support for TS on the library side is very solid[1].

Of course, it's still based on a dynamic language and has dynamic elements to it. It's always possible to violate the type system (heck, you can toss a comment in and violate it on a given line if you wish), but the problem you're describing I haven't remembered being a large source of grief, and most of the issues I used to run into from time to time aren't issues in recent versions.

If I misread your scenario, let me know; I've been pretty amazed at the kinds of things I can express with their type system and how well its compiler enforces those things[2] so I'm curious about where it really falls down.

[0] The specifics elude me, but I recall it had to do with the line that wrapped the component; this was three years ago and I had never re-examined the issue after that.

[1] Though I'd not encountered a component/library that I couldn't find a solid alternative to or workaround in some way to get it working in TS.

[2] It'd be nice if those extended in a manner that would handle things one encounters with Json data not quite matching up type-wise but a lot of that is because "it doesn't know how the object is going to be used/loaded at runtime" and you can't really expect it to do so.


I think you understood me correctly. Thanks for the various thoughts.

A bit hard to provide an mvc of the issue I'm thinking of. But I often see it happen with HOC kinda stuff. Like I might have a loader component, that I pass three things to: the query to run, a component to render, and some additional props to the component to render. The render component will get the props, plus the result of the query as well.

When adding typing to this, ts will naturally complain. So fix that by adding generics etc. And then everything looks fine according to the ts compiler, so you're happy.

But if you try to change the typing of the inner component, or some type of the props being sent to the outer component, you might find that ts is still happy. Somewhere along the line, it decided to throw its hands up and allow everything. So the typing is just complex noise, not catching subtle bugs.

Of course, people could easily write a correct example handling this. But the problem is that there are so many complex ways to solve stuff in ts, and some of them are wrong even though they look correct.

Some of it might be fixed using a stricter setup as you mention.


FWIW, today we specifically teach and recommend the React-Redux hooks API as the default, and one of the major reasons is that it works _so_ much better with TypeScript than the earlier `connect` API:

https://redux.js.org/tutorials/typescript-quick-start


I was a Flow holdout for the longest time. TypeScript's support for discriminated unions is one of the things which finally won me over. Super glad to see it being improved upon here.


Flow also supports "discriminated unions" [0], and did so before TypeScript (Flow's announcement in July 2015: https://flow.org/blog/2015/07/03/Disjoint-Unions/, TypeScript's announcement in August 2016: https://devblogs.microsoft.com/typescript/announcing-typescr...), though it doesn't yet support destructuring the "kind" field as was just added to TypeScript.

[0] https://flow.org/try/#0C4TwDgpgBAggxsAlgewHZQLwCgq6gHygG8oBr...


As somebody who never worked with Flow but loves TS, what does Flow offer that the latter can’t do?


1. And biggest for me: Exact types

    function x(y: {
      foo: number,
      bar?: number
    }) {}

    const y = {
      foo: 1,
      baar: 2, // typo
    }
    x(y)
The above is caught in Flow when exact types are enabled but not in TS.

TS issue https://github.com/microsoft/TypeScript/issues/12936

TS has a concept of "freshness" which catches some of these mistakes when you construct y right in the arguments of x, but it gets easily broken by accident and without a warning.

2. Nominal types, so you can have for example "Id" type that is a number but not to be mixed with other numbers, you can't just call a function that expects an Id with literal 42. Classes are nominal too, which I think is a solid choise.

3. Type variance & related safeguards

    function x(y: {
      foo: string | number
    }) {
      y.foo = 1
    } 
    const y: {
      foo: string
    } = {foo: 'a'} 
    x(y) 
    y.foo.toUpperCase()
Caught by Flow but not TS.

4. Local call site type inference of function argument types, so you can have some helper functions within a file that you don't need to explicitly annotate, and Flow will happily infer types based on how you call it.

5. Performance has gotten prerty good with recent versions, to the point where I suspect it's faster than TS. I don't have any hard data on this though.

Even though in many many ways Flow is worse than TS, we have still stuck with it, and that's only like 80% because it'd be s huge undertaking to make the switch. 20% is because some of these features are actually nice :)


I would also like nominal types in TypeScript, but one issue is that it might open a can of worms that is antithetical to one of TypeScript’s main design goals: that a change in the typing of a program should not be able to modify behavior.

If you add nominal types, some people might expect them to be boxed, and thus be able to reflect on types at runtime. So I assume that TypeScript maintainers have made a design decision that users should have to “self-box” nominal types.


>2. Nominal types, so you can have for example "Id" type that is a number but not to be mixed with other numbers, you can't just call a function that expects an Id with literal 42. Classes are nominal too, which I think is a solid choise.

Structural typing is one of the best features of TS in my opinion, I like it in general but it's a better fit for JS ecosystem.


Structural typing is a sane default in JS land, but there are times that you really really want nominal typing and its complete absence from TS kind of hurts.


FWIW, io-ts offers this via its “userland” implementation of branded types.


for what it's worth you can totally fake nominal typing in a hacky way through a sort of shiboleth key. You say "this type has `____type: 'foo'`, and on constructions of the object you just cast (or hell, just put the key in!), and you're off to the races.

This is far from perfect, but when you're working with very tricky code and want to be sure, you at least have the option


Yeah, for modeling with types, you frequently want nominal types (e.g. you want to make sure that a certain types always means “the data has passed through such and such a function). Flow’s combination of structural and nominal types seems better to me than TS’s pure structural system.


I worked on a project that used used Flow 3 years or so ago. Unless something has changed since then, it was crazy fast. It took a couple of seconds from typing a command in the console to type-check the project to getting the results; whereas running tsc on a project I am working on now takes dozens of seconds to complete. This is my deepest regret of leaving Flow.


Hmm, TS is super fast today for me, at least in all the cases that matter for me. I rarely run a full project typecheck because I can trust my IDE to show me errors instantly.


Speaking of IDEs (well, VSCode), one minor annoyance is that I have to manually restart the typescript language server from time to time; otherwise VSCode doesn't pick up the types properly. Don't know which part of the toolchain is to blame for this.


Same here, usually after rebuilding linked packages.


Same thing here, though we use TS in a pretty specific way.


Another example would be the difference between their enum features: https://medium.com/flow-type/typescript-enums-vs-flow-enums-... "TypeScript Enums vs. Flow Enums"


Nominal/opaque primitive types. You “can” do it with hacks but it’s some kind of a fib and depending on approach it’s less type safe.


it has some nice features that ts doesn't have: opaque types (can be done by nominal tying in ts). More stringent JSX, but only slightly.

I think that's it these days. Flow used to have (slightly) more features but now TS has way more already and even more to come. The Flow people moved on to Recript, or whatever name they call it these days.


FYI, it would not be accurate to say that "The Flow people moved on to ReScript".


Flow feels pretty dead. The VSCode extension is broken in major ways and hasn’t gotten a major update in years. The only mainstream editor that fully supports it is IntelliJ.


The extension has picked up some attention from the core team, ever since FB retired their Nuclide tool for Atom. Not saying that the extension is perfect, but AFAIK most features come through LSP and are added to the Flow server and should pretty much be just picked up by the extension now that the LSP wiring is done. So apart from bugfixes and other small stuff, I don't know what kind of major updates you are expecting to the thing.


Explanation:

A record is a set of key-value pairs with the value having a given type (also called a struct, object, etc. depending on the context). In TypeScript, this is an example of a record:

    const foo = { bar: 1, baz: "qux" };
Destructuring is an ergonomic feature where you can take the fields of a record and assign them to local variables. For example:

    // Equivalent to
    // const bar = foo.bar;
    // const baz = foo.baz;
    const { bar, baz } = foo;
Besides ergonomics, destructuring can offer features like exhaustiveness-checking, to make sure that you used all of the fields of a record.

A union type is a type which denotes that the value is one of two different types, indicated like this:

    type Foo = number | string;
Sometimes, you have two different cases with the same type, and you want to distinguish between them. For example:

    // Not helpful -- we can't tell from the value
    // whether it's supposed to be a username or an email.
    type UsernameOrEmail = string | string;
To handle this case, you can use a discriminated union instead. These unions have a special value which unambiguously indicates which case is present:

    type UsernameOrEmail = { type: "username", value: string }
                         | { type: "email", value: string };
Sometimes, you have a discriminated union with different fields:

    type ApiRequest = { type: "message", subject: string, contents: string }
                    | { type: "view-posts", user_id: number };
Currently, TypeScript uses control-flow analysis to ensure that you're using the right kinds of fields depending on the discriminator value:

    const request: ApiRequest = getRequest();
    if (request.type === "message") {
        // Okay:
        console.log("Subject is", request.subject);
        // Rejected by compiler:
        console.log("User ID is", request.user_id);
    }
However, you can't combine this feature with destructuring:

    type Action = { kind: "A", payload: number }
                | { kind: "B", payload: string };
    
    const { kind, payload } = getAction();
    if (kind === "B") {
        // Not currently allowed:
        payload.toUpperCase();
    }
With this pull request, you can do the above. This can be convenient with patterns like reducers (see https://github.com/microsoft/TypeScript/issues/46143 for an example).


Thanks for the explanation. I've been programming for many years, and your examples make total sense to me, but reading sentences like "Control flow analysis for destructured discriminated unions" make me feel like a total impostor


It’s just like any other programming problem, break it into parts, and build it back up again.

“What’s a union?”

“Okay… then what’s a discriminated union?”

“Okay… let’s apply destructuring to one…”

“Okay… now what would analysis look like there given what I know?”

“Okay… how do we have the computer know what I know intuitively as a human given this context?”

“Okay! I think I got it!”

:-). If you’ve gotten this far with programming to understand the examples above, my guess is if you just take the time you can do it without the examples if you take it in small enough chunks :-).

Life’s all about what you apply your energy too

(And also, wonderful example :-) )


I know you're entirely right, but sometimes when I see things like this in Typescript...

https://github.com/sindresorhus/type-fest/blob/main/source/r...

I can walk through it with enough pause, but it kinda makes my head implode when I glance at it.


  { [Key in KeysType]-?: ... }
TIL that you can disable ? and readonly in mapped types using -. So in the above code, every Key will be mandatory (-?) even if the original one in KeysType was optional.

source: https://www.typescriptlang.org/docs/handbook/2/mapped-types....


Yea it’s like reading Shakespeare… I can read the words and know what most of them mean… but damn if it doesn’t take serious energy to actually parse out the meaning sometimes.

I guess in a way, the issue is that it is cloaked with familiarity. Your brain gets so used to understanding sentences on first go it’s not used to not understanding it.

However, if it was a chemistry equation and you weren’t familiar with chemistry it would immediately understand that comprehension would take time and thus the mind implosion due to a lack of understanding doesn’t occur.


I think most of us would feel like impostors next to Anders Hejlsberg.


You and me both buddy, you and me.


Thank you for the explanation, very clear and concise.


That's a great explanation, thank you for that.


Union types seem a bit wacky, are they typically used over function overloading or generics?


An example of union types: many API in JS accept HTTP verbs as strings. With union types, you can say something like:

    type HttpVerb = "GET" | "POST" | "PUT" | "DELETE";
But you can also do more complex stuff:

    type User = {name: string; password: strings} | OAuthID;
By themselves they aren't hard to understand or use.

As an aside, compared to sum types, you don't have to define them before using them: a function can return number | null while in language with sum types you usually define option first and then return option<number>.


You might find my blog post on some of the uses interesting: https://blog.waleedkhan.name/union-vs-sum-types/

In a language like TypeScript, they can be used for function overloading since there is no runtime dispatch based on argument type. But usually they're used as sum types in the same way as e.g. Rust enums.

Typescript supports so-called "singleton types", in which a given type is inhabited by exactly one value. This lets you do things like specify not just `string` but specific values like `"foo" | "bar"` as a type. In these cases, I often use them in combination with Typescript's "mapped types", which let you index into a map with a certain type and get another type out. This can be useful for things like RPC calls, where the type of the output depends on the type of the input.


>since there is no runtime dispatch based on argument type

Ah, this answers my question I guess. Thanks for the info. I'm about to start a new job doing Typescript stuff so I am going to be deep diving into the language to get my head around it, so this helps a lot :-)


Union types let you accomplish very similar things to what you can do in languages that support Enums with associated values, like Rust. (See https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html for examples of what that looks like. There's no great direct comparison in Java, because Java Enums don't support associated values like this; the closest Java pattern would involve making multiple classes that each extend an interface or abstract class.)


The most common thing to use them for in TS is to acknowledge a value could be of type null or undefined. Let’s you then check you’re handling them correctly.


Something I am absolutely missing from TypeScript is enums with associated data like in Rust: https://doc.rust-lang.org/reference/items/enumerations.html.

The usefulness of this obviously depends on pattern matching control flow ops in Rust that don't exist in TypeScript but it would make my life just so much easier.


You can do a similar thing with discriminated unions in TS, with some caveats mainly around ergonomics. You can even get exhaustiveness checking with a little trickery

Edit, example of exhaustiveness checking:

  switch(obj.kind) {
    case "one": return ...
    case "two": return ...
    case "three": return ...
    default:
      // @ts-expect-error
      throw Error("Didn't cover " + obj.kind)
  }
This will actually err at compile time if you missed a case, because otherwise obj.kind will have type "never", which will cause a type error on that line, which will be expected due to the directive comment. If obj.kind is not "never", the code will not have an error, and so the directive will cause an error.

...it is definitely preferable having language-level support for this stuff though.


In most situations you don't even need a default case: https://tsplay.dev/NlpBGN


That's what union types are.

Actually, in practice I find TS union types much more useful than Rust enums, because you can mix and match options in different combinations freely. In Rust, you can't use one of the enum variants as a type on it's own, and I had to tediously create a lot of wrapper types that I didn't in TS.


They are not the same feature. Enums allows you to create new values, while union types only allow you to combine values. For example, in Rust, enum Toto { Titi, Tata } would create a new type, Toto, that contains two values, Toto::Titi and Toto::Tata. There's no way to express that with union types.

On the other hand, Rust's enum have to be defined before use: I can't suddently decide to have a function that returns "Toto" | "Tata" without creating a type or returning string.

OCaml has something called "polymorphic variants" that seem to be a middle-ground between the two: https://ocaml.org/manual/polyvariant.html.


Help me understand this feature. How does it differ from a discriminated union of object types? (I might be completely misunderstanding the feature you’re raising)


TS discriminated unions are functionally nearly identical to variants in OCaml and enums in Rust. I don't know about Rust, but the difference between TS and OCaml is mainly just the syntax and ergonomics. OCaml has great syntax for pattern matching and exhaustiveness checking, for example. You can still get this functionality in TS with if/else or switch statements and potentially some type system tricks: https://www.fullstory.com/blog/discriminated-unions-and-exha...

To illustrate how TS discriminated unions are functionally equivalent, check out this playground link showing how ReScript (an alternate syntax of OCaml which can compile to JavaScript) compiles an OCaml variant to a JavaScript object with a "TAG" key that could easily be typed as a discriminated union in TypeScript:

https://rescript-lang.org/try?code=C4TwDgpgBAThCOBXCBnYBlYBD...


Wow. More impressive work by the Typescript team!

AFAICT this will have the most impact on folks using Typescript with Redux stores and similar dispatchers.I know I would have liked having this feature back when I was doing that work.


FWIW, our standard recommended Redux usage patterns for the last couple years have specifically _not_ needed switch statements to determine how to handle an action, because our official Redux Toolkit package has a `createSlice` API that lets you define case reducers as simple functions in an object. `createSlice` then generates all the action types internally, generates corresponding action creator functions, and handles calling the right case reducer when that action is dispatched.

Additionally, RTK has been designed to work great with TypeScript. Typically all you need to declare is the type of the state for that slice, and the payload for each reducer's action, and everything else is inferred.

See our Redux docs tutorials for details:

https://redux.js.org/tutorials/essentials/part-2-app-structu...

https://redux.js.org/tutorials/fundamentals/part-8-modern-re...

https://redux.js.org/tutorials/typescript-quick-start


I believe this will also obviate a common recommendation to not destructure when using React Query: https://tkdodo.eu/blog/react-query-and-type-script#type-narr...


Thanks for sharing this. I literally ran into the same issue yesterday, and was wondering why TS can't understand that I am discriminating on the isSucces property.


I’ll definitely update that article once the new TS version is released :)


Note that this already worked fine by just operating on the field.

Like `if (action.kind === 'x')` / `switch (action.kind)`.

This "only" extends support to destructured discriminators.

Definitely nice to have, but of limited impact.


Definitely a help. It should make it easier to use with payloads in postMessage too.



It's really cool that this is possible. TypeScript has developed an incredibly advanced type system in only a few years.


But there is always that coworker who still insists on using any.


Common problem with emerging technologies (such as TypeScript for JavaScript development) is that many junior or even intermediate developers try to shoehorn their "newly learned about tech" into every single problem, slowing everyone and everything down because of this hype-chasing. So even for simple applications that won't benefit anything from type checking now gets wrapped in TypeScript. Then some people find that because you're using TypeScript, you need everything to be precisely typed, when you (as a human) might know better and just type the areas where it's the most important, so you can move forward faster.

But as always, context is king and no solution fits everything. But hype makes it seem like a tool fits everything, to the detriment of everyone else.


This is awesome! Thank you to the PR author for working on this — it is one of the most common pain points I have encountered when writing TypeScript.


Looks like pattern matching!


Kinda like that.

Simple example:

Lets say you can submit one of these payloads

{ type: 'SET_PERSON', payload: Person } and { type: 'SET_AGE', payload: number }

Based on what type field equals to, TLS (Typescript Language Server - used by IDEs) and TSC (TypeScript Compiler) will know type of payload. It’s already possible to achieve that to some extent, but it’s really limited.


I love that TypeScript never stops pushing type safety.


Any body know what is @typescript-bot ? How to setup one for my project ?

@typescript-bot pack this @typescript-bot test this @typescript-bot user test this @typescript-bot run dt @typescript-bot perf test this


It's probably a simple bot that's hooked up to a GH app. Then just some custom code to react to specific types of events. Depending on what you need done, you'd possibly be able to replicate a lot of functionality within a GH action.


Yep, that is exactly what it is, just a simple GH webhook to azure develops / GitHub Actions trigger. https://github.com/microsoft/TypeScript/issues/46177#issueco...


Seems like a good way of adding additional comment spam to your projects. What happened with having a GUI with audit logs that you can click a button/make a curl request to in order to trigger?

God, 19 out of 22 comments are all about CI and should not even be in the PR. How can people effectively work with so much noise in their PRs?


Yes! I also found myself wishing for this very feature. TypeScript continues to get more and more awesome.


This is sweet ! Now i can truly have the Strategy pattern implemented with TS.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: