Skip to content

Matchbox Usage

One of the most powerful features of Matchbox is exhaustive pattern matching with the match method, serving as an alternative to switch statements that are not guaranteed to be exhaustive.

Given the following matchbox factory:

export const shapes = matchboxFactory({
Circle: (radius: number) => ({ radius }),
Square: (side: number) => ({ side }),
Rectangle: (width: number, height: number) => ({ width, height }),
});

The most powerful feature of tagged unions created with matchboxFactory is exhaustive pattern matching with the match method:

export const
const area: number
area
=
const someShape: MatchboxMember<"Circle", {
Circle: (radius: number) => {
radius: number;
};
Square: (side: number) => {
side: number;
};
Rectangle: (width: number, height: number) => {
width: number;
height: number;
};
}, "type">
someShape
.
MatchboxMemberApi<{ Circle: (radius: number) => { radius: number; }; Square: (side: number) => { side: number; }; Rectangle: (width: number, height: number) => { width: number; height: number; }; }, "type">.match<number>(cases: MatchCases<{
Circle: (radius: number) => {
radius: number;
};
Square: (side: number) => {
side: number;
};
Rectangle: (width: number, height: number) => {
width: number;
height: number;
};
}, number, true>, exhaustive?: boolean): number (+1 overload)
match
({
type Circle: ({ radius }: {
radius: number;
}) => number
Circle
: ({
radius: number
radius
}) =>
var Math: Math
Math
.
Math.PI: number
PI
*
radius: number
radius
*
radius: number
radius
,
type Square: ({ side }: {
side: number;
}) => number
Square
: ({
side: number
side
}) =>
side: number
side
*
side: number
side
,
type Rectangle: ({ width, height }: {
width: number;
height: number;
}) => number
Rectangle
: ({
width: number
width
,
height: number
height
}) =>
width: number
width
*
height: number
height
,
});

Sometimes you want to handle multiple states with a single case, or provide a fallback:

export const
const cornerCount: number
cornerCount
=
const someShape: MatchboxMember<"Circle", {
Circle: (radius: number) => {
radius: number;
};
Square: (side: number) => {
side: number;
};
Rectangle: (width: number, height: number) => {
width: number;
height: number;
};
}, "type">
someShape
.
MatchboxMemberApi<{ Circle: (radius: number) => { radius: number; }; Square: (side: number) => { side: number; }; Rectangle: (width: number, height: number) => { width: number; height: number; }; }, "type">.match<number>(cases: MatchCases<{
Circle: (radius: number) => {
radius: number;
};
Square: (side: number) => {
side: number;
};
Rectangle: (width: number, height: number) => {
width: number;
height: number;
};
}, number, true>, exhaustive?: boolean): number (+1 overload)
match
({
type Circle: () => number
Circle
: () => 0,
_?: ((...args: any[]) => number) | undefined
_
: () => 4, // Default case for Square and Rectangle
});

You can opt-out of exhaustiveness checking by passing false as the second argument:

export const
const optionalNickname: string
optionalNickname
=
const someShape: MatchboxMember<"Circle", {
Circle: (radius: number) => {
radius: number;
};
Square: (side: number) => {
side: number;
};
Rectangle: (width: number, height: number) => {
width: number;
height: number;
};
}, "type">
someShape
.
MatchboxMemberApi<{ Circle: (radius: number) => { radius: number; }; Square: (side: number) => { side: number; }; Rectangle: (width: number, height: number) => { width: number; height: number; }; }, "type">.match<string>(cases: MatchCases<{
Circle: (radius: number) => {
radius: number;
};
Square: (side: number) => {
side: number;
};
Rectangle: (width: number, height: number) => {
width: number;
height: number;
};
}, string, true>, exhaustive?: boolean): string (+1 overload)
match
(
{
type Circle: ({ radius }: {
radius: number;
}) => string
Circle
: ({
radius: number
radius
}) => `Circle with radius ${
radius: number
radius
}`,
type Square: ({ side }: {
side: number;
}) => string
Square
: ({
side: number
side
}) => `Square with side ${
side: number
side
}`,
},
false
);

Using type guards in Matchbox provides several benefits:

  • Type safety: Prevent runtime errors by catching type mismatches at compile time
  • Code completion: Get proper IDE suggestions based on narrowed types
  • Refactoring safety: When you change a state’s structure, TypeScript will flag all places that need updates
  • Exhaustive checking: Ensure all possible states are handled with pattern matching

Tagged union instances created with matchboxFactory provide automatic type narrowing with the is method:

// Create a Light matchbox with On/Off states
const
const Light: MatchboxFactory<{
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
Light
=
matchboxFactory<{
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag", MatchboxFactory<{
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">>(config: {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, tagProp?: "tag" | undefined): MatchboxFactory<{
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
matchboxFactory
({
type Off: undefined
Off
:
var undefined
undefined
,
type On: (percentage?: number) => {
percentage: number;
}
On
: (
percentage: number
percentage
= 100) => ({
percentage: number
percentage
}),
});
// Create light instances
const
const light1: MatchboxMember<"Off", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
light1
=
const Light: MatchboxFactory<{
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
Light
.
type Off: () => MatchboxMember<"Off", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
Off
();
const
const light2: MatchboxMember<"On", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
light2
=
const Light: MatchboxFactory<{
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
Light
.
type On: (percentage?: number | undefined) => MatchboxMember<"On", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
On
(75);
// Type guards with automatic type narrowing
if (
const light2: MatchboxMember<"On", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
light2
.
MatchboxMemberApi<{ Off: undefined; On: (percentage?: number) => { percentage: number; }; }, "tag">.is: <"On">(key: "On") => this is MatchboxMember<T, DataSpecs, TagProp>
is
("On")) {
// TypeScript knows light2.data has percentage
var console: Console
console
.
Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
log
(`Brightness: ${
const light2: MatchboxMember<"On", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
light2
.
data: {
percentage: number;
}
data
.
percentage: number
percentage
}%`);
// This would be a TypeScript error:
// console.log(light2.data.invalid); // Error: Property 'invalid' does not exist
}
// Type guards can be used in conditions
function
function getBrightness(light: ReturnType<typeof Light.Off> | ReturnType<typeof Light.On>): number
getBrightness
(
light: MatchboxMember<"Off", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag"> | MatchboxMember<"On", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
light
:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
ReturnType
<typeof
const Light: MatchboxFactory<{
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
Light
.
type Off: () => MatchboxMember<"Off", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
Off
> |
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
ReturnType
<typeof
const Light: MatchboxFactory<{
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
Light
.
type On: (percentage?: number | undefined) => MatchboxMember<"On", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
On
>
) {
if (
light: MatchboxMember<"Off", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag"> | MatchboxMember<"On", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
light
.
MatchboxMemberApi<DataSpecs, TagProp extends string>.is: <"Off">(key: "Off") => this is MatchboxMember<T, DataSpecs, TagProp>
is
("Off")) {
return 0;
}
// TypeScript knows this must be the "On" state with percentage
return
light: MatchboxMember<"On", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
light
.
data: {
percentage: number;
}
data
.
percentage: number
percentage
;
}
var console: Console
console
.
Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
log
(
function getBrightness(light: ReturnType<typeof Light.Off> | ReturnType<typeof Light.On>): number
getBrightness
(
const light1: MatchboxMember<"Off", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
light1
)); // 0
var console: Console
console
.
Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
log
(
function getBrightness(light: ReturnType<typeof Light.Off> | ReturnType<typeof Light.On>): number
getBrightness
(
const light2: MatchboxMember<"On", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
light2
)); // 75

Use the as method for type-safe casting with runtime validation:

import {
function matchboxFactory<Config extends TaggedTypes | string, TagProp extends string = "tag", R = MatchboxFactory<Config extends readonly string[] ? { [K in Config[number]]: (data: any) => any; } : Config, TagProp>>(config: Config, tagProp?: TagProp): R
matchboxFactory
} from "matchina";
// Create a Light matchbox with On/Off states
const
const Light: MatchboxFactory<{
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
Light
=
matchboxFactory<{
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag", MatchboxFactory<{
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">>(config: {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, tagProp?: "tag" | undefined): MatchboxFactory<{
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
matchboxFactory
({
type Off: undefined
Off
:
var undefined
undefined
,
type On: (percentage?: number) => {
percentage: number;
}
On
: (
percentage: number
percentage
= 100) => ({
percentage: number
percentage
}),
});
const
const light: MatchboxMember<"On", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
light
=
const Light: MatchboxFactory<{
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
Light
.
type On: (percentage?: number | undefined) => MatchboxMember<"On", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
On
(80);
// Safe casting with runtime validation
try {
// This works because light is actually in the "On" state
const
const asOn: MatchboxMember<"On", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
asOn
=
const light: MatchboxMember<"On", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
light
.
MatchboxMemberApi<{ Off: undefined; On: (percentage?: number) => { percentage: number; }; }, "tag">.as: <"On">(key: "On") => MatchboxMember<"On", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
as
("On");
var console: Console
console
.
Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
log
(`Brightness: ${
const asOn: MatchboxMember<"On", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
asOn
.
data: {
percentage: number;
}
data
.
percentage: number
percentage
}%`); // Works fine: "Brightness: 80%"
// This will throw because light is not in the "Off" state
const
const asOff: MatchboxMember<"Off", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
asOff
=
const light: MatchboxMember<"On", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
light
.
MatchboxMemberApi<{ Off: undefined; On: (percentage?: number) => { percentage: number; }; }, "tag">.as: <"Off">(key: "Off") => MatchboxMember<"Off", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
as
("Off");
var console: Console
console
.
Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
log
("This line will never execute",
const asOff: MatchboxMember<"Off", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
asOff
);
} catch (
var e: any
e
: any) {
var console: Console
console
.
Console.error(message?: any, ...optionalParams: any[]): void (+1 overload)
error
("Cast failed:",
var e: any
e
.
any
message
); // "Attempted to cast On as Off"
}
// Practical example: only adjust brightness for "On" lights
export function
function adjustBrightness(light: ReturnType<typeof Light.Off> | ReturnType<typeof Light.On>, adjustment: number): MatchboxMember<"On", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag"> | MatchboxMember<"Off", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
adjustBrightness
(
light: MatchboxMember<"On", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag"> | MatchboxMember<"Off", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
light
:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
ReturnType
<typeof
const Light: MatchboxFactory<{
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
Light
.
type Off: () => MatchboxMember<"Off", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
Off
> |
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
ReturnType
<typeof
const Light: MatchboxFactory<{
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
Light
.
type On: (percentage?: number | undefined) => MatchboxMember<"On", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
On
>,
adjustment: number
adjustment
: number
) {
try {
// Try to cast to "On" state
const
const onLight: MatchboxMember<"On", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
onLight
=
light: MatchboxMember<"On", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag"> | MatchboxMember<"Off", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
light
.
MatchboxMemberApi<DataSpecs, TagProp extends string>.as: <"On">(key: "On") => MatchboxMember<"On", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
as
("On");
// If successful, create a new light with adjusted brightness
const
const newBrightness: number
newBrightness
=
var Math: Math
Math
.
Math.max(...values: number[]): number
max
(
0,
var Math: Math
Math
.
Math.min(...values: number[]): number
min
(100,
const onLight: MatchboxMember<"On", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
onLight
.
data: {
percentage: number;
}
data
.
percentage: number
percentage
+
adjustment: number
adjustment
)
);
return
const Light: MatchboxFactory<{
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
Light
.
type On: (percentage?: number | undefined) => MatchboxMember<"On", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
On
(
const newBrightness: number
newBrightness
);
} catch {
// Light is off, return it unchanged
return
light: MatchboxMember<"On", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag"> | MatchboxMember<"Off", {
Off: undefined;
On: (percentage?: number) => {
percentage: number;
};
}, "tag">
light
;
}
}

Matchina also provides filter functions for more complex type guards:

const Shape = matchbox({
Circle: (radius: number, color: string = "black") => ({ radius, color }),
Rectangle: (width: number, height: number, color: string = "black") => ({
width,
height,
color,
}),
Triangle: (base: number, height: number, color: string = "black") => ({
base,
height,
color,
}),
});
const shapes = [
Shape.Circle(5, "red"),
Shape.Rectangle(10, 20, "blue"),
Shape.Circle(8, "green"),
Shape.Triangle(15, 10, "red"),
];
// Filter by key
const circles = shapes.filter(withKey("Circle"));
console.log(`Found ${circles.length} circles`);
// Filter by data property
const redShapes = shapes.filter(withData((data) => data.color === "red"));
console.log(`Found ${redShapes.length} red shapes`);
// Combine filters
const largeCircles = shapes.filter(
(shape) => shape.is("Circle") && shape.data.radius > 7,
);
console.log(`Found ${largeCircles.length} large circles`);

Now that you understand type guards, explore these related guides: