How to typescript: Representing hierarchical state with tuples

For our latest feature in ahead, messages, the UI is in one of three major states:

type MessagingState = 
  | "IDLE" 
  | "STARTING_CONVERSATION" 
  | "ACTIVE_CONVERSATION";

A couple of iterations and additions later, the mode in which there is an active conversation had two sub-states, depending whether you are writing and looking at the latest messages or are browsing older messages. Some UI parts are only really interested in whether there is currently a conversation active or not.

One could expand the state of the UI with an additional property denoting that sub-state:

{
  mainMode: MessagingState;
  subMode: "SYNCED" | "EXPLORING" | null;
}

While this certainly works, there is no quick way to consolidate into the type that the sub-state is only valid for one of the main states and not for the other.

So, here’s another way to represent the dependency between main and sub-state, namely via tuples:

type MessagingState = 
  | ["IDLE"] 
  | ["STARTING_CONVERSATION"] 
  | ["ACTIVE_CONVERSATION", "SYNCED" | "EXPLORING"];

It is now clear that only one of the main states has two further sub-states. However, checking against in which state we are can become cumbersome, as we can’t just do an equality check of the state value but must rather check against the members of the tuple.

To overcome this, a helper function is introduced:

function matches<
  T extends [string] | [string,string], 
  X extends T[0], 
  Y extends T[1]>(
  value: T,
  check: X,
  check2?: Extract<T, [X, Y]>[1],
) {
  return value[0] === check && (check2 ? value[1] === check2 : true);
}

it accepts

The way the type of the function is defined allows to “check” the checks one writes down at compile-time. That is, the function only supports calls that correspond to valid states:

const state = ["IDLE"] as MessagingState;

matches(state, "IDLE") // Compiles
matches(state, "BLA") // Doesn't Compile
matches(state, "ACTIVE_CONVERSATION") // Compiles
matches(state, "ACTIVE_CONVERSATION", "SYNCED") // Compiles
matches(state, "ACTIVE_CONVERSATION", "BLA") // Doesn't Compile
matches(state, "IDLE", "SYNCED") // Doesn't Compile

With this kind of state definition, UI code can now quickly check whether some state is given in general, or can make a stricter check whether some sub-state is currently set or not.

If you want to play around with this code, you can visit this playground.