[HN Gopher] TypeScript: Control flow analysis for destructured d...
       ___________________________________________________________________
        
       TypeScript: Control flow analysis for destructured discriminated
       unions
        
       Author : tosh
       Score  : 111 points
       Date   : 2021-11-03 19:40 UTC (3 hours ago)
        
 (HTM) web link (github.com)
 (TXT) w3m dump (github.com)
        
       | the_gipsy wrote:
       | Looks like pattern matching!
        
         | NaughtyShiba wrote:
         | 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.
        
       | arxanas wrote:
       | 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).
        
         | dekerta wrote:
         | 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
        
           | coolgoose wrote:
           | You and me both buddy, you and me.
        
         | friedman23 wrote:
         | Thank you for the explanation, very clear and concise.
        
       | sakesun wrote:
       | 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
        
         | Master_Odin wrote:
         | 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.
        
       | 19h wrote:
       | 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.
        
         | brundolf wrote:
         | 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.
        
         | Waterluvian wrote:
         | 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)
        
           | tshaddox wrote:
           | 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...
        
         | golergka wrote:
         | 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.
        
       | sizediterable wrote:
       | Also: https://github.com/microsoft/TypeScript/pull/46429. Anders
       | is on a roll!
        
       | aluminum96 wrote:
       | It's really cool that this is possible. TypeScript has developed
       | an incredibly advanced type system in only a few years.
        
       | bluepnume wrote:
       | 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.
        
         | gherkinnn wrote:
         | As somebody who never worked with Flow but loves TS, what does
         | Flow offer that the latter can't do?
        
       | 015a wrote:
       | 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.
        
         | eyelidlessness wrote:
         | 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
        
         | gherkinnn wrote:
         | 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...
        
       | maxfurman wrote:
       | 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.
        
         | tshaddox wrote:
         | 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...
        
           | dragosmocrii wrote:
           | 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.
        
         | NaughtyShiba wrote:
         | Definitely a help. It should make it easier to use with
         | payloads in postMessage too.
        
         | the_duke wrote:
         | 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.
        
         | acemarke wrote:
         | 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
        
       | revskill wrote:
       | This is sweet ! Now i can truly have the Strategy pattern
       | implemented with TS.
        
       ___________________________________________________________________
       (page generated 2021-11-03 23:00 UTC)