1. 5
    1. 3

      I eschew switch. The JS switch syntax feels particularly archaic next to the rest of the nice typelevel stuff in TS. Instead, we use many-arm if-elseif-else with predicate functions, which is a simpler and more general pattern, because it allows branching for groups of types instead of relying on an exhaustive list of concrete values in all cases.

      Here’s what I mean:

      // You can imagine each member of the union being an object
      // with the specific attributes for that element
      type OutlineEl = 'header' | 'nav' | 'footer' | 'article'
      type InlineEl = 'strong' | 'em'
      type CustomEl = 'div' | 'span'
      type El = OutlineEl | InlineEl | CustomEl
      
      // Making these correct by construction left as an exercise to the reader
      function isOutlineElement(e: El): e is OutlineEl { return true }
      function isInlineElement(e: El): e is InlineEl { return true }
      function isCustomElement(e: El): e is CustomEl { return true }
      
      function doSomethingWithElement(e: El) {
        if (isInlineElement(e)) {
          // Handle all inline elements in a general way
          // With a switch statement, we'd need a label for every member of the union's
          // discriminant repeated explicitly in source code.
          return
        } else if (isOutlineElement(e)) {
          // handle outline elements in a general way
          return
        } else if (e === 'div') {
          // We can still handle specific cases naturally
          // specifically handle div
          return
        } else if (e === 'span') {
          // specifically handle span
          return
        }
      
        // And we still get type narrowing down to never for exhaustive case checking.
        assertNever(e)
      }
      
      // Copied from the article.
      function assertNever(x: never): never {
        throw new Error("The impossible has happened:" + x);
      }
      

      See it on Typescript Playground

      1. 2

        Okay, okay, here’s the way to implement the type assertions in a way that’s correct by construction, with no stuttering or duplicating of any string literals:

        const isOutlineElMap = {
          header: true,
          nav: true,
          footer: true,
          article: true,
        } as const
        
        const isInlineElMap = {
          strong: true,
          em: true,
        } as const
        
        const isCustomElMap = {
          div: true,
          span: true,
        } as const
        
        type OutlineEl = keyof typeof isOutlineElMap
        type InlineEl = keyof typeof isInlineElMap
        type CustomEl = keyof typeof isCustomElMap
        type El = OutlineEl | InlineEl | CustomEl
        
        function isOutlineEl(e: El): e is OutlineEl {
          return e in isOutlineElMap
        }
        
        // Making a higher-order function to produce isInlineEl and
        // isCustomEl from an isElMap left as an exercise to the reader.
        

        See it on Typescript Playground

    2. 1

      I hope they add something like pattern matching to make checking through the cases of the ADT a bit less cumbersome. Given how much ceremony it takes to define and use an ADT, I sometimes wonder if I’m even doing the right thing.

      1. 1

        The switch statement is kind of a weak pattern matching. It doesn’t allow you to match on the fields or extract field values easily. I still consider it a good tool for your toolbox, specially if you do Redux. In Redux, you have ADTs, either explicitly declared, or implicitly declared. I prefer explicit in terms of types, so at least the typechecker can spot some of my mistakes.

    3. 1

      This is even better in F# (compiled to ECMAScript with Fable). I highly recommend evaluating Fable for your ES needs if you are currently using TypeScript but like this sort of thing.

      type MyAdt =
          | Option1 of fooField : string
          | Option2 of barField : float * bazField : string
          | Option3 of fooField : float * bazField : string
      
      let doSomething (value : MyAdt) =
          match value with
          | Option1 (fooField) ->
              printfn "Option 1 fields: %s" fooField
          | Option2 (barField, bazField) ->
              printfn "Option 2 fields: %f, %s" barField bazField
          | Option3 (fooField, bazField) ->
              printfn "Option 3 fields: %f, %s" fooField bazField
      

      or, perhaps slightly less conveniently, with records:

      type MyAdt =
          | Option1 of
              {| 
                  fooField : string 
              |}
          | Option2 of 
              {| 
                  barField : float
                  bazField : string
              |}
          | Option3 of
              {|
                  fooField : float
                  bazField : string
              |}
      
      let doSomething (value : MyAdt) =
          match value with
          | Option1 fields ->
              printfn "Option 1 fields: %s" fields.fooField
          | Option2 fields ->
              printfn "Option 2 fields: %f, %s" fields.barField fields.bazField
          | Option3 fields ->
              printfn "Option 3 fields: %f, %s" fields.fooField fields.bazField
      

      We don’t need the “impossible” case because the compiler (naturally) checks pattern matches for exhaustion.

      Worlds longest link to Fable playground with this code.

      1. 1

        How does Fable compare to Reason/OCaml?