[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)