Least Tiresome Exhaustive Matching in ST's MX


Overview: matchAs()()()

matchAs is a type-safe pattern matching utility for discriminated unions. It never misses a case, it never drops to a fallback. It's completely prepared for future code changes. It uses nearly zero JavaScript, but hides some advanced TypeScript to know exactly what state your data is in.


Installation

npm install @arksouthern/stx

Import

import matchAs from '@arksouthern/stx/mx'

Basic Example

type AccountTab = { as: "accountTab", userInfo: DetailedUserInfo }
type FriendsTab = { as: "friendsTab", userFriendList: Friends[] }
type MessageTab = { as: "messageTab", selectedMessageThread: Msg[] }
type MxTab = AccountTab | FriendsTab | MessageTab
function selectTab(tab: MxTab) {
return matchAs(tab)({
accountTab: x => userInfoHtml(x.userInfo),
friendsTab: x => x.userFriendList[0],
messageTab: x => checkLastMsg(x)
})(tab)
}

You can safely write a function that take a union, different kinds of data, then use matchAs to exactly handle every case.
If you add cases, or remove them, in the future, then instantly your matchAs will alert you in TypeScript that an update is needed.


Key Features

No Fallback Cases, No Missing Cases Either

// ❌ TypeScript Error: Property 'messageTab' is missing
matchAs(tab)({
accountTab: x => userInfoHtml(x.userInfo),
friendsTab: x => x.userFriendList[0]
// Missing messageTab handler!
})(tab)

Always Exact Type, No Duck Checking, No Unknowns

matchAs(tab)({
accountTab: x => {
// x is AccountTab
x.userInfo // is available here
},
friendsTab: x => {
// x is FriendsTab
x.userFriendList // is available here
},
messageTab: x => {
// x is MessageTab
x.selectedMessageThread // is available here
}
})(tab)

Return Type Inference

// Return type: HTMLElement | Friends | boolean
const result = matchAs(tab)({
accountTab: x => userInfoHtml(x.userInfo), // returns HTMLElement
friendsTab: x => x.userFriendList[0], // returns Friends
messageTab: x => checkLastMsg(x) // returns boolean
})(tab)

Performance-First In Action

// Create matcher once
const matchTab = matchAs({} as MxTab)({
accountTab: x => userInfoHtml(x.userInfo),
friendsTab: x => x.userFriendList[0],
messageTab: x => checkLastMsg(x)
});
// Reuse in loop
tabs.forEach(tab => {
const result = matchTab(tab)
// Process result...
})

Instructions

1. Type Requirements
Your discriminated union must:

  • Use as for the discriminant property name
  • Have string literal types for the as values
  • Define as on all variants
// ✅ Correct
type Good =
| { as: "option1", data: string }
| { as: "option2", count: number }
// ❌ Won't work - uses 'type' instead of 'as'
type Bad =
| { type: "option1", data: string }
| { type: "option2", count: number }

2. Usage matchAs works with discriminated unions that use an as property as their discriminant. It enforces exhaustive matching: TypeScript will error if any variant is unhandled.

matchAs uses a curried API with three stages:

  1. matchAs(union) - Pass a representative value from your union type
  2. ({ ... handlers ... }) - Provide handlers for each variant
  3. (value) - Pass the actual value to match

Exhaustive Pattern Matching with matchAs()
  
  
  
  
(1) Setting up the matcher

          ┌─────────────┐ 
          │             │ 
          │  matchAs()  │ 
          │             │ 
          └──────┬──────┘ 
                 │       
                 │       ┌──────────────────┐
                 │       │ union: U         │
                 |       |                  |
                 │       │ (discriminant)   │
                 │       └────────┬─────────┘
                 │                │
                 └───────────────►│
                                  │
                                  ▼
                         ┌─────────────────┐
                         │ Matcher Factory │
                         └────────┬────────┘
                                  │


(2) Providing exhaustive handlers

                         ┌────────────────────────────┐
                         │ handlers: {                │
                         │                            │
                         │   variantA: (x) => ...,    │
                         │                            │
                         │   variantB: (x) => ...,    │
                         │                            │
                         │   variantC: (x) => ...     │
                         │                            │
                         │ }                          │
                         └──────────┬─────────────────┘
                                    │
                                    │ TypeScript enforces
                                    |
                                    │ all variants present
                                    │
                                    ▼
                           ┌─────────────────┐
                           │ Runtime Matcher │
                           └────────┬────────┘
                                    │


(3) Matching on actual value

                           ┌─────────────────┐
                           │ value: V        │
                           |                 |
                           │ (actual data)   │
                           └────────┬────────┘
                                    │
                                    │ value.as determines
                                    |
                                    │ which handler runs
                                    │
                                    ▼
                  ┌─────────────────────────────────┐
                  │ handlers[value.as](value)       │
                  │                                 │
                  │ Returns: ReturnType<F[V["as"]]> │
                  └─────────────────────────────────┘


(4) Full flow example

    matchAs(tab)              ({                        (currentTab)
                                accountTab: x => ...,          
         │                      friendsTab: x => ...,         │
         │                      messageTab: x => ...          │
         │                    })                              │
         │                      │                             │
         └─────────────────────►└────────────────────────────►│
                                                              │
                      ┌───────────────────────────────────────┘
                      │
                      ▼
         ┌─────────────────────────────┐
         │ Type-safe, exhaustive match │
         |                             |
         │ with full inference         │
         └─────────────────────────────┘

Exhaustive Matching Using ST's MX
ArkSouthern

AuthoredApr 3rd 2022
UpdatedJan 25th 2026
IDn-rd-stg