1. 5
  1.  

  2. 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.

      2. 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?