Tagged Union in React.Js with TypeScript and how to respect your props.

zenobio

Zenobio

Posted on April 23, 2021

Tagged Union in React.Js with TypeScript and how to respect your props.

If you've ever used languages like Elm or ReasonML for writing any front-end application then you are probably familiar with the terms Tagged Union, Variants or even Discriminated Unions, but if it is not the case, let me show what i'm referring to:

-- Full code example at: https://ellie-app.com/cYzXCP7WnNDa1

-- FieldType is a Tagged union.
type FieldType
  = Editable
  | ViewOnly

init : () -> (Model, Cmd Msg)
init _ =
    (
      initial,
      (ViewOnly, "Welcome to ELM")
        |> Task.succeed
        |> Task.perform Init
    )

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    Init (kind, value) ->
      (
        { model | value = value, kind = kind }
        , Cmd.none
      )

    Handle value ->
      (
        { model | value = value }
        , Cmd.none
      )


view : Model -> Html Msg
view { kind, value } =
    case kind of
        Editable ->
          div []
            [
              input [onInput Handle] []
            , h1 [] [text ("Value: " ++ value)]
            ]

        ViewOnly ->
          div [] [  h1 [] [ text value ] ]
Enter fullscreen mode Exit fullscreen mode

The code above displays one of Elm's main strengths when we talk about modeling your application based on data types.

Don't be afraid of all the boilerplate, the main point here is how we have a completely agnostic view while also being 100% sure that our model can't and won't be in a undetermined state or have any missing props, never.

Our model property kind will never contain anything different from a FieldType and with the help of the Elm compiler we could rest assure that our view will also be reliable and always have all the needed data.

Typescript

Today, Typescript have been massively used as a tool which helps in minimize some runtime errors and give some guarantees about what exactly are our Data inside the sea of uncertainty that is Javascript code.

That being said, let's take a look on how commonly components are validated in some React with Typescript code bases:


// FieldType could also be just the strings values.
enum FieldType {
  VIEW_ONLY = "viewOnly",
  EDITABLE = "editable"
};

type Props = {
  kind: FieldType;
  onChange: (_: ChangeEvent<HTMLInputElement>) => void;
  name?: string;
  value: string;
};

const Field: VFC<Props> = (props) => {

// ...component implementation
};
Enter fullscreen mode Exit fullscreen mode

The compiler will prevent you from use the component without the required props, but, do you really need a onChange function if you just want a non editable field?

What about any new member which enters the team, how will this component plays when someone with no deep understanding of every and each component in the code base tries to use it somewhere else?

Sure, the code above just shows a simple Field component, nothing that we couldn't reason about just reading the code, but, it is far from a good implementation if you do want to respect the props, the component behavior for each kind of implementation and how it will play when it is needed somewhere else.

Tagged Unions for the rescue.

"Talk is cheap, show me the code", Linus Torvalds

enum FieldType {
  VIEW_ONLY = "viewOnly",
  EDITABLE = "editable"
};

type BaseProps = {
  kind: FieldType;
  name?: string;
  value: string;
};

type Editable = {
  kind: FieldType.EDITABLE;
  onChange: (_: ChangeEvent<HTMLInputElement>) => void;
} & BaseProps;

type ViewOnly = {
 kind: FieldType.VIEWONLY;
} & BaseProps;

type Props = ViewOnly | Editable;

const Field: VFC<Props> = (props) => {
  const { value, name, kind } = props;
  const { onChange } = props as Editable;

  // ...component implementation
}
Enter fullscreen mode Exit fullscreen mode

Now, we have some extra type boilerplate but our component props would be respected and enforced by a known FieldType just like we intended to when the component was implemented. Now, what if we try to call our component without any property? what will happen?

Well, at first, Typescript will show you an error in compile time;

Type '{}' is not assignable to type '(IntrinsicAttributes & { kind: FieldType.VIEW_ONLY; } & DefaultProps) | (IntrinsicAttributes & { kind: FieldType.EDITABLE; onChange: (_: ChangeEvent<...>) => void; } & DefaultProps)'.
  Type '{}' is missing the following properties from type '{ kind: FieldType.EDITABLE; onChange: (_: ChangeEvent<HTMLInputElement>) => void; }': type, onChange
Enter fullscreen mode Exit fullscreen mode

Then,after you provide the property kind with a known FieldType, it will show you which properties you still need to provide to ensure that your component has everything it needs to work as expected:

...
Property 'onChange' is missing in type '{ kind: FieldType.EDITABLE; }' but required in type '{ kind: FieldType.EDITABLE; onChange: (_: ChangeEvent<HTMLInputElement>) => void; }'.
Enter fullscreen mode Exit fullscreen mode

Now, you just need to use a mapped object or a switch case in your Field component render, which, based on the enforced prop kind and given your nullable or not nullable props enforced by the tagged union, it'll show you exactly what needs and not needs to be handled, formatted or treated.

For the sake of reusability the FieldType enum can be moved to a types/field or a types/components.

Here's an implementation for example pourposes:

Final Thoughts

IMHO that's one of the best ways to really use the Typescript compiler to help us compose code.

Not only validating our components within nullable or non nullable props values, but also helping in the proper implementation while also being thoughtful of the ones who will come to use, maintain and update the code base.

💖 💪 🙅 🚩
zenobio
Zenobio

Posted on April 23, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related