Home

Awesome

@core/match

[ GitHub tani/ts-match ] [ JSR @core/match ]

A pattern matching library for JavaScript/ TypeScript

EcmaScript has a structured binding. It is a very useful notation for extracting only the necessary parts from a complex structure. However, this structured binding is incomplete for use as pattern matching. Because, To do a structured binding, the pre-assigned value must match the pattern, If it does not match the pattern, an exception is thrown.

const { a } = JSON.parse("null"); // ERROR!

Therefore, to do structured binding, TypeScript either guarantees that the structure matches at compile time, or it does not, validation libraries such as zod or unknownutil checks at runtime that the structure matches. The former is impotent for data whose structure is not determined at compile time, such as JSON data. The latter required writing two structured binding patterns and two validation patterns.

This library can perform structured binding and validation simultaneously, while preserving compile-time type information. It is a library that enables true pattern matching and brings EcmaScript's structured binding to perfection.

Again, this is not just for TypeScript, it is also useful in JavaScript.

Usage

This library is published in JSR and can be used in Deno with jsr:@core/match. There are only two functions that users need to remember: _ and match.

import { match, placeholder as _ } from "jsr:@core/match";

How to declare type guards.

In TypeScript, a type guard is a function with type (v: unknown) => v is T, It can be declared as follows. There is also a collection of generic type guards, such as unknownutil.

function isNumber(v: unknown): v is number {
  return typeof v === "number";
}

License.

This library is licensed under the MIT License. Please feel free to use it as long as you comply with the licence.


Scenarios for using this library

First, load the library.

import { match, placeholder as _ } from "./mod.ts";
import { assertEquals } from "jsr:@std/assert";

This library can be used to check if an object matches a specific pattern. The following example demonstrates how to match an object with a simple string value. If the object matches the pattern, the result will be an empty object, since the pattern does not contain any placeholders yet.

Deno.test("match object with primitive string value", () => {
  const pattern = "hello";
  const value = "hello";
  const result = match(pattern, value);
  assertEquals(result, {});
});

You can also use numeric values as patterns.

Deno.test("match object with primitive number value", () => {
  const pattern = 123;
  const value = 123;
  const result = match(pattern, value);
  assertEquals(result, {});
});

Boolean values also can be used as patterns.

Deno.test("match object with primitive boolean value", () => {
  const pattern = true;
  const value = true;
  const result = match(pattern, value);
  assertEquals(result, {});
});

Null values can be used as patterns as well. Note that the match function returns an empty object even if both the pattern and the value are null.

Deno.test("match object with primitive null value", () => {
  const pattern = null;
  const value = null;
  const result = match(pattern, value);
  assertEquals(result, {});
});

Undefined values can also be used as patterns. Note that the match function returns an empty object even if both the pattern and the value are undefined.

Deno.test("match object with primitive undefined value", () => {
  const pattern = undefined;
  const value = undefined;
  const result = match(pattern, value);
  assertEquals(result, {});
});

Symbol values can be used as patterns as well.

Deno.test("match object with primitive symbol value", () => {
  const symbol = Symbol("hello");
  const pattern = symbol;
  const value = symbol;
  const result = match(pattern, value);
  assertEquals(result, {});
});

You can also use compound objects as patterns. If the object matches the pattern, the result will still be an empty object, because there are no placeholders in the pattern.

Deno.test("match object with compound object value", () => {
  const pattern = { name: "hello", age: 1 };
  const value = { name: "hello", age: 1 };
  const result = match(pattern, value);
  assertEquals(result, {});
});

Arrays can be used as patterns too. If the object matches the pattern, the result will still be an empty object, because there are no placeholders in the pattern.

Deno.test("match object with array value", () => {
  const pattern = ["hello", 123];
  const value = ["hello", 123];
  const result = match(pattern, value);
  assertEquals(result, {});
});

If the object does not match the pattern, the match function returns undefined. In the following pattern, the value of the object does not match the pattern because the value is not object.

Deno.test("match object with primitive value (not equal)", () => {
  const pattern = { a: 1 };
  const value = 123;
  const result = match(pattern, value);
  assertEquals(result, undefined);
});

Now, let's use a placeholder. The first step is to declare a single placeholder. The placeholder is declared using the _ function, and the name of the placeholder is passed as an argument. The placeholder holds the name's string value and the type guard function test. The following placeholder does not have a type guard, making it the simplest form of a placeholder.

Deno.test("declare single placeholder", () => {
  const pattern = _("a");
  assertEquals(pattern.name, "a");
  assertEquals(pattern.test, undefined);
});

To use the placeholder, apply the match function. The match function returns an object containing the key-value pair of the placeholder's name and the associated object's value. If the object does not match the pattern, the match function returns undefined. Note that the resulting object has an a key, the value of the object is hello, and its type is unknown in TypeScript because the placeholder has no type guard.

Deno.test("match object with single placeholder", () => {
  const pattern = _("a");
  const value = "hello";
  const result = match(pattern, value);
  assertEquals(result, { a: "hello" });
});

To provide a type guard for the placeholder, declare the type guard function as the second argument of the _ function.

Deno.test("match object with single placeholder and type guard", () => {
  const pattern = _("a", (v: unknown): v is string => typeof v === "string");
  const value = "hello";
  const result = match(pattern, value);
  assertEquals(result, { a: "hello" });
});

If the type guard fails, the match function returns undefined.

Deno.test("match object with single placeholder and type guard", () => {
  const pattern = _("a", (v: unknown): v is string => typeof v === "string");
  const value = 123;
  const result = match(pattern, value);
  assertEquals(result, undefined);
});

Placeholders can also be used in compound objects. The following pattern has two placeholders, a and b. Note that the resulting object has a and b keys, and the values of the object are hello and 123 respectively. Furthermore, the type of the a value is unknown, and the type of the b value is unknown in TypeScript.

Deno.test("match object with compound object and placeholders", () => {
  const pattern = { name: _("a"), age: _("b") };
  const value = { name: "hello", age: 1 };
  const result = match(pattern, value);
  assertEquals(result, { a: "hello", b: 1 });
});

Similarly, you can pass the type guard to the placeholder. In this case, the type of the a value is string, while the type of the b value is number in TypeScript.

Deno.test("match object with compound object and placeholders (type guard)", () => {
  const pattern = {
    name: _("a", (v: unknown): v is string => typeof v === "string"),
    age: _("b", (v: unknown): v is number => typeof v === "number"),
  };
  const value = { name: "hello", age: 1 };
  const result = match(pattern, value);
  assertEquals(result, { a: "hello", b: 1 });
});

It is expected that the match function returns undefined if the object does not match the pattern. In the following pattern, there are two placeholders, a and b. The value of the object does not match the pattern because the name key is missing. Therefore, the match function returns undefined.

Deno.test("match object with compound object and placeholders (missing key)", () => {
  const pattern = { name: _("a"), age: _("b") };
  const value = { age: 1 };
  const result = match(pattern, value);
  assertEquals(result, undefined);
});

You can also use the placeholder to skip the key. The following pattern has two placeholders, anonymous placeholder and b. Note that the resulting object has only b key, and the value of the object is 1. Furthermore, the type of the b value is unknown in TypeScript because the placeholder has no type guard.

Deno.test("match object with compound object and placeholders (skip key)", () => {
  const pattern = { name: _(), age: _("b") };
  const value = { name: "john", age: 1 };
  const result = match(pattern, value);
  assertEquals(result, { b: 1 });
});

As the same as other cases, the anonymous placeholder can be used with the type guard. The following pattern has two placeholders, anonymous placeholder and b. Note that the resulting object has only b key, and the value of the object is 1. Furthermore, the type of the b value is unknown in TypeScript because the placeholder has no type guard.

Deno.test("match object with compound object and placeholders (skip key, type guard)", () => {
  const pattern = {
    name: _((v: unknown): v is string => typeof v === "string"),
    age: _("b"),
  };
  const value = { name: "john", age: 1 };
  const result = match(pattern, value);
  assertEquals(result, { b: 1 });
});

The match function returns undefined if the object does not match the pattern. The following pattern has two placeholders, anonymous placeholder and b. The value of the object does not match the pattern because the type guard fails. Therefore, the match function returns undefined.

Deno.test("match object with compound object and placeholders (skip key, type guard, fail)", () => {
  const pattern = {
    name: _((v: unknown): v is string => typeof v === "string"),
    age: _("b"),
  };
  const value = { name: 1, age: 1 };
  const result = match(pattern, value);
  assertEquals(result, undefined);
});

In other cases, the match function returns undefined if the type guard fails. The following pattern has two placeholders, a and b. The value of the object does not match the pattern because the age value is not a number. Therefore, the match function returns undefined.

Deno.test("match object with compound object and placeholders (type guard fail)", () => {
  const pattern = {
    name: _("a", (v: unknown): v is string => typeof v === "string"),
    age: _("b", (v: unknown): v is number => typeof v === "number"),
  };
  const value = { name: "hello", age: "123" };
  const result = match(pattern, value);
  assertEquals(result, undefined);
});

Placeholders can be used in arrays as well. The following pattern has two placeholders, a and b. Note that the resulting object has a and b keys, and the values of the object are hello and 123 respectively. Furthermore, the type of the a value is unknown, and the type of the b value is unknown in TypeScript.

Deno.test("match object with array and placeholders", () => {
  const pattern = [_("a"), _("b")];
  const value = ["hello", 123];
  const result = match(pattern, value);
  assertEquals(result, { a: "hello", b: 123 });
});

You can pass the type guard to the placeholder in the same way as before, so the type of the a value is string, and the type of the b value is number in TypeScript.

Deno.test("match object with array and placeholders (type guard)", () => {
  const pattern = [
    _("a", (v: unknown): v is string => typeof v === "string"),
    _("b", (v: unknown): v is number => typeof v === "number"),
  ];
  const value = ["hello", 123];
  const result = match(pattern, value);
  assertEquals(result, { a: "hello", b: 123 });
});

It is expected that the match function returns undefined if the object does not match the pattern. The following pattern has two placeholders, a and b. The value of the object does not match the pattern because the name key is missing.

Deno.test("match object with array and placeholders (missing key)", () => {
  const pattern = [_("a"), _("b")];
  const value = [123];
  const result = match(pattern, value);
  assertEquals(result, undefined);
});

In other cases, the match function returns undefined if the type guard fails. The following pattern has two placeholders, a and b. The value of the object does not match the pattern because the age value is not a number.

Deno.test("match object with array and placeholders (type guard fail)", () => {
  const pattern = [
    _("a", (v: unknown): v is string => typeof v === "string"),
    _("b", (v: unknown): v is number => typeof v === "number"),
  ];
  const value = ["hello", "123"];
  const result = match(pattern, value);
  assertEquals(result, undefined);
});

Placeholders can be used in both objects and arrays at the same time. The following pattern has two placeholders, a and b. Note that the resulting object has a and b keys, and the values of the object are hello and 123 respectively. Furthermore, the type of the a value is unknown, and the type of the b value is unknown in TypeScript.

Deno.test("match object with object, array, and placeholders", () => {
  const pattern = { name: _("a"), age: [_("b")] };
  const value = { name: "hello", age: [123] };
  const result = match(pattern, value);
  assertEquals(result, { a: "hello", b: 123 });
});

In the same manner, you can pass the type guard to the placeholder. Now, the type of the a value is string, and the type of the b value is number in TypeScript.

Deno.test("match object with object, array, and placeholders (type guard)", () => {
  const pattern = {
    name: _("a", (v: unknown): v is string => typeof v === "string"),
    age: [_("b", (v: unknown): v is number => typeof v === "number")],
  };
  const value = { name: "hello", age: [123] };
  const result = match(pattern, value);
  assertEquals(result, { a: "hello", b: 123 });
});

It is expected that the match function will return undefined if the object does not match the pattern. In the following pattern, there are two placeholders, a and b. The value of the object does not match the pattern because the name key is missing.

Deno.test("match object with object, array, and placeholders (missing key)", () => {
  const pattern = { name: _("a"), age: [_("b")] };
  const value = { age: [123] };
  const result = match(pattern, value);
  assertEquals(result, undefined);
});

In other cases, the match function returns undefined if the type guard fails. The following pattern has two placeholders, a and b. The value of the object does not match the pattern because the age value is not a number.

Deno.test("match object with object, array, and placeholders (type guard fail)", () => {
  const pattern = {
    name: _("a", (v: unknown): v is string => typeof v === "string"),
    age: [_("b", (v: unknown): v is number => typeof v === "number")],
  };
  const value = { name: "hello", age: ["123"] };
  const result = match(pattern, value);
  assertEquals(result, undefined);
});

As a niche feature, pattern matching with placeholders can be used with user-defined classes. Note that the resulting object has name and age keys, and the values of the object are hello and 123 respectively. Furthermore, the type of the name value is unknown, and the type of the age value is number in TypeScript because the placeholder has no type guard for the name value.

class User {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}
Deno.test("match object with user-defined class and placeholders", () => {
  const pattern = {
    name: _("name"),
    age: _("age", (v: unknown): v is number => typeof v === "number"),
  };
  const value = new User("hello", 123);
  const result = match(pattern, value);
  assertEquals(result, { name: "hello", age: 123 });
});

In the same manner, you can pass the type guard to the placeholder. Now, the type of the name value is string, and the type of the age value is number in TypeScript.

Deno.test("match object with user-defined class and placeholders (type guard)", () => {
  const pattern = {
    name: _("name", (v: unknown): v is string => typeof v === "string"),
    age: _("age", (v: unknown): v is number => typeof v === "number"),
  };
  const value = new User("hello", 123);
  const result = match(pattern, value);
  assertEquals(result, { name: "hello", age: 123 });
});

It is expected that the match function will return undefined if the object does not match the pattern. In the following pattern, there are two placeholders, name and age. The value of the object does not match the pattern because the name key is missing.

Deno.test("match object with user-defined class and placeholders (missing key)", () => {
  const pattern = { name: _("name"), age: _("age") };
  const value = new User("hello", 123);
  const result = match(pattern, value);
  assertEquals(result, { name: "hello", age: 123 });
});

Additionally, if the pattern is not a direct instance of an Object or an Array, the match function tries to check the equality of the pattern and the value using the === operator. Hence, the following pattern does not match the value, and the match function returns undefined.

Deno.test("match object with primitive string value (not equal)", () => {
  const pattern = new User("hello", 123);
  const value = new User("hello", 123);
  const result = match(pattern, value);
  assertEquals(result, undefined);
});

The match function also supports template string placeholders. The following pattern has a template string placeholder, name. Note that the resulting object has a name key, and the value of the object is john. The type of the name value is string in TypeScript because the placeholder has no type guard.

Deno.test("match object with template string placeholder", () => {
  const pattern = _`hello ${_("name")}`;
  const value = "hello john";
  const result = match(pattern, value);
  assertEquals(result, { name: "john" });
});

You can also pass the type guard to the placeholder in the same way as before. Now, the type of the name value is string in TypeScript.

Deno.test("match object with template string placeholder (type guard)", () => {
  const pattern = _`hello ${
    _("name", (v: unknown): v is string => typeof v === "string")
  }`;
  const value = "hello john";
  const result = match(pattern, value);
  assertEquals(result, { name: "john" });
});

The match function returns undefined if the object does not match the pattern. The following pattern has a template string placeholder, answer. The value of the object does not match the pattern because the value is not a number.

Deno.test("match object with template string placeholder (type guard, fail)", () => {
  const isNumString = (v: unknown): v is `${number}` =>
    typeof v === "string" && !isNaN(Number(v));
  const pattern = _`1 + 1 = ${_("answer", isNumString)}`;
  const value = "1 + 1 = infinity";
  const result = match(pattern, value);
  assertEquals(result, undefined);
});

The positive case of the type guard is also tested. The following pattern has a template string placeholder, answer. The value of the object matches the pattern, and the match function returns an object containing the key-value pair of the placeholder's name and the associated object's value. Note that the resulting object has an answer key, and the value of the object is 2. The type of the answer value is '${numver}', which is a string shape of a number in TypeScript.

Deno.test("match object with template string placeholder (type guard)", () => {
  const isNumString = (v: unknown): v is `${number}` =>
    typeof v === "string" && !isNaN(Number(v));
  const pattern = _`1 + 1 = ${_("answer", isNumString)}`;
  const value = "1 + 1 = 2";
  const result = match(pattern, value);
  assertEquals(result, { answer: "2" });
});

It is expected that the match function will return undefined if the object does not match the pattern. In the following pattern, there is a template string placeholder, name. The value of the object does not match the pattern because the length of the value is not equal to the length of the pattern.

Deno.test("match object with template string placeholder (length not match)", () => {
  const pattern = _`hello ${_("name")}`;
  const value = "hello";
  const result = match(pattern, value);
  assertEquals(result, undefined);
});

In other cases, the match function returns undefined. The following pattern has a template string placeholder, name. The value of the object does not match the pattern because the value is not a string.

Deno.test("match object with template string placeholder (not string)", () => {
  const pattern = _`hello ${_("name")}`;
  const value = 123;
  const result = match(pattern, value);
  assertEquals(result, undefined);
});

The match function also supports template string placeholders with multiple placeholders. The following pattern has two template string placeholders, name and age. Note that the resulting object has name and age keys, and the values of the object are john and 123 respectively. The type of the name value is string, and the type of the age value is string in TypeScript because the placeholders have no type guards.

Deno.test("match object with template string placeholder with multiple placeholders", () => {
  const pattern = _`${_("name")} is ${_("age")} years old`;
  const value = "john is 123 years old";
  const result = match(pattern, value);
  assertEquals(result, { name: "john", age: "123" });
});

Match funciton returns undefined if the object does not match the pattern. The following pattern has two template string placeholders, name and age. The value of the object does not match the pattern because the value is shorter than the pattern.

Deno.test("match object with template string placeholder with multiple placeholders (not equal)", () => {
  const pattern = _`${_("name")} is ${_("age")} years old`;
  const value = "john is 123 years";
  const result = match(pattern, value);
  assertEquals(result, undefined);
});

The match function also supports template string placeholders with greedy mode. The following pattern has two template string placeholders, name and age. Note that the resulting object has name and age keys, and the values of the object are john and 123 respectively. The type of the name value is string, and the type of the age value is string in TypeScript because the placeholders have no type guards.

Deno.test("match object with template string placeholder with multiple placeholders (greedy, not equal)", () => {
  const pattern = _.greedy`${_("name")} is ${_("age")} years old`;
  const value = "john is 123 years old";
  const result = match(pattern, value);
  assertEquals(result, { name: "john", age: "123" });
});

Ultimately, the match function could run with a pattern that contains a regular placeholder and a template string placeholder. The following pattern has a regular placeholder, address, and a template string placeholder, message. Note that the resulting object has address, name, and age keys, and the values of the object are 123, john, and 123 respectively. The type of the address value is string, and the type of the name and age values are string in TypeScript because the placeholders have no type guards.

Deno.test("match object with a regular placeholder with the template string placeholder", () => {
  const pattern = {
    address: _("address"),
    message: _`${_("name")} is ${_("age")} years old`,
  };
  const value = {
    address: "123",
    message: "john is 123 years old",
  };
  const result = match(pattern, value);
  assertEquals(result, { address: "123", name: "john", age: "123" });
});