Tagged Union in React.Js with TypeScript and how to respect your props.
Tags: react, javascript, functional, typescript
Published: 2021-04-23 18:48:38

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:

```elm
-- 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 ] ]
```

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:

```tsx

// 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
};
```

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

```jsx
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
}
```

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;

```ts
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
```

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:

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

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:
{% codesandbox discriminatedunioncomponent-xmb47 %}

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