@endo/patterns

中文文档
2.1.0发布于 2026/08/14Apache-2.0TypeScriptES Module近 30 天下载 3.5万

Pattern matching for Passable objects, expressed as Passable data

原始使用文档
文档目录26 节
  1. Overview
  2. Quick Start
  3. The M Namespace
  4. Primitive Matchers
  5. Container Matchers
  6. Structured Matchers
  7. Logical Operators
  8. Comparison Matchers
  9. Special Matchers
  10. Pattern Matching
  11. matches(specimen, pattern)
  12. mustMatch(specimen, pattern, label?)
  13. Copy Collections
  14. CopySet
  15. CopyBag
  16. CopyMap
  17. Interface Guards
  18. Creating Interface Guards
  19. Method Guard Structure
  20. Integration with Exo
  21. Key Comparison
  22. keyEQ(key1, key2)
  23. compareKeys(key1, key2)
  24. Key, Pattern, and Passable Hierarchy
  25. Integration with Endo Packages
  26. Deep Dives

@endo/patterns

Pattern matching and validation for passable data, with copy-collections and interface guards.

Overview

The @endo/patterns package provides the M namespace for creating pattern matchers that validate passable data and describe behavioral contracts. This is the validation layer above @endo/pass-style, enabling you to check that data matches expected shapes before using it.

Patterns enable:

  • Data validation: Check that values match expected types and structures
  • Interface contracts: Describe method signatures with InterfaceGuards
  • Copy collections: CopySet, CopyBag, CopyMap for passable data structures
  • Key comparison: Distributed equality for comparing values across vats

Quick Start

import { M, mustMatch } from '@endo/patterns';

const specimen = harden({ foo: 3, bar: 4 });

const pattern = M.splitRecord(
  { foo: M.number() },                        // required properties
  { bar: M.string(), baz: M.number() }       // optional properties
);

mustMatch(specimen, pattern);
// throws: 'bar?: number 4 - Must be a string'

For best rendering, use the Endo reference docs site.

The M Namespace

The M object provides methods for creating pattern matchers organized into several categories:

Primitive Matchers

Match specific JavaScript types:

M.any()           // Matches any passable
M.undefined()     // Matches undefined
M.null()          // Matches null
M.boolean()       // Matches true or false
M.number()        // Matches any number (including NaN, Infinity)
M.bigint()        // Matches any bigint
M.string()        // Matches any string
M.symbol()        // Matches registered/well-known symbols

// Constrained primitives
M.nat()           // Non-negative bigint
M.gte(5)          // Number >= 5
M.lte(100)        // Number <= 100

Container Matchers

Match copyArray, copyRecord, and other structures:

M.array()         // Any CopyArray
M.record()        // Any CopyRecord
M.set()           // Any CopySet
M.bag()           // Any CopyBag
M.map()           // Any CopyMap

// With constraints
M.array({ maxSize: 10 })          // Array with at most 10 elements
M.string({ maxSize: 100 })        // String with at most 100 characters

// Structured content
M.arrayOf(M.number())             // Array of numbers only
M.recordOf(M.string(), M.number()) // Record with string keys, number values
M.setOf(M.string())               // Set of strings only

Structured Matchers

Match specific shapes:

// Split patterns: required, optional, rest
M.splitArray(
  [M.string(), M.number()],      // required elements
  [M.boolean()],                  // optional elements
  M.any()                         // rest elements
)

M.splitRecord(
  { name: M.string() },           // required properties
  { age: M.number() },            // optional properties
  M.any()                         // rest properties
)

// Partial matches
M.partial({ name: M.string() })  // Has at least 'name' property

// Split auto-detects array vs record
M.split({ x: M.number() }, M.any())

Logical Operators

Combine matchers:

M.and(M.number(), M.gte(0), M.lte(100))   // 0 <= n <= 100
M.or(M.string(), M.number())               // String or number
M.not(M.undefined())                       // Anything except undefined
M.opt(M.string())                          // undefined or string (optional)

Comparison Matchers

Match values relative to a key:

M.eq('hello')     // Equal to 'hello'
M.neq(0)          // Not equal to 0
M.lt(10)          // Less than 10
M.lte(100)        // Less than or equal to 100
M.gte(0)          // Greater than or equal to 0
M.gt(-1)          // Greater than -1

Special Matchers

M.remotable()           // Any remotable object
M.remotable('Counter')  // Remotable with specific label
M.error()               // Any error
M.promise()             // Any promise
M.eref(M.number())      // Number or promise for number (eventual reference)
M.kind('copyArray')     // Specific pass style
M.pattern()             // Any valid pattern
M.key()                 // Any valid Key
M.scalar()              // Any primitive or remotable

Pattern Matching

matches(specimen, pattern)

Returns true if the specimen matches the pattern, false otherwise:

import { M, matches } from '@endo/patterns';

matches(42, M.number());                    // true
matches('hello', M.number());               // false
matches([1, 2, 3], M.arrayOf(M.number())); // true

mustMatch(specimen, pattern, label?)

Throws with a descriptive error if the specimen doesn't match:

import { mustMatch } from '@endo/patterns';

mustMatch(42, M.string());
// throws: "number 42 - Must be a string"

mustMatch(-5, M.and(M.number(), M.gte(0)), 'count');
// throws: "count: number -5 - Must be >= 0"

The error messages are designed to help you understand exactly what was wrong with the data.

Copy Collections

Patterns introduces three passable collection types built on makeTagged():

CopySet

A set of unique Keys (primitives or remotables):

import { makeCopySet } from '@endo/patterns';

const colors = makeCopySet(['red', 'blue', 'green']);

// Elements are sorted in rank order
// Duplicates are removed
// Can be passed between vats

// Pattern for sets
const ColorSet = M.setOf(M.string());
mustMatch(colors, ColorSet);  // passes

Why not use JavaScript Set? JavaScript Sets aren't passable. CopySet is frozen, comparable via keyEQ, and can be efficiently serialized.

CopyBag

A multiset (elements with counts):

import { makeCopyBag } from '@endo/patterns';

const inventory = makeCopyBag([
  ['apples', 5n],
  ['oranges', 3n],
  ['apples', 2n]   // counts are combined
]);

// Result: [['apples', 7n], ['oranges', 3n]]

const InventoryPattern = M.bagOf(M.string(), M.bigint());
mustMatch(inventory, InventoryPattern);

CopyMap

A map from Keys to Passable values:

import { makeCopyMap } from '@endo/patterns';

const balances = makeCopyMap([
  ['alice', 100],
  ['bob', 50]
]);

// Keys are sorted in rank order
// Can use any Key as a key (not just strings!)

const remotableKey = Far('Key', {});
const map = makeCopyMap([[remotableKey, 'value']]);

const BalancesPattern = M.mapOf(M.string(), M.number());
mustMatch(balances, BalancesPattern);

Why not use plain objects? CopyMap supports:

  • Any Key as a key (objects, remotables, not just strings)
  • Efficient key comparison using compareKeys()
  • Subset relationships for partial ordering

Interface Guards

InterfaceGuards describe behavioral contracts for objects, particularly useful with @endo/exo:

Creating Interface Guards

import { M } from '@endo/patterns';

const CounterI = M.interface('Counter', {
  // Synchronous method
  increment: M.call(M.number()).returns(M.number()),

  // Method with optional arguments
  reset: M.call().optional(M.number()).returns(),

  // Method with rest arguments
  add: M.call(M.number()).rest(M.number()).returns(M.number()),

  // Async method (awaits arguments)
  asyncOp: M.callWhen(M.string()).returns(M.string())
});

Method Guard Structure

// Basic call: call(required args...)
M.call(M.string(), M.number())

// With optional args
M.call(M.string()).optional(M.number())

// With rest args
M.call(M.string()).rest(M.any())

// Specify return type
M.call(M.string()).returns(M.number())

// Async method (awaits promise args)
M.callWhen(M.remotable()).returns(M.string())

Integration with Exo

InterfaceGuards are enforced automatically by exos:

import { makeExo } from '@endo/exo';
import { M } from '@endo/patterns';

const CounterI = M.interface('Counter', {
  increment: M.call(M.number()).returns(M.number())
});

const counter = makeExo('Counter', CounterI, {
  increment(n) {
    // n is guaranteed to be a number by the guard
    return count += n;
  }
});

counter.increment(5);      // OK
counter.increment('5');    // throws: Must be a number

This is the foundation of defensive programming in Endo: guards validate inputs automatically, so your methods can focus on business logic.

Key Comparison

Keys can be compared for equality and ordering:

keyEQ(key1, key2)

Tests if two Keys are equal using distributed equality semantics:

import { keyEQ } from '@endo/patterns';

keyEQ('hello', 'hello');        // true
keyEQ(42, 42);                  // true
keyEQ([1, 2], [1, 2]);          // true (compares content)

const r1 = Far('Obj', {});
const r2 = Far('Obj', {});
keyEQ(r1, r1);                  // true (same remotable)
keyEQ(r1, r2);                  // false (different remotables)

compareKeys(key1, key2)

Returns a comparison result implementing a partial order:

  • 0: Keys are equal
  • -1: key1 < key2
  • 1: key1 > key2
  • NaN: Keys are incomparable
import { compareKeys, keyLT, keyGT } from '@endo/patterns';

compareKeys('a', 'b');          // -1
compareKeys(5, 5);              // 0
compareKeys(10, 3);             // 1

// Convenience functions
keyLT('a', 'b');                // true
keyGT(10, 3);                   // true

// Incomparable keys
const r1 = Far('A', {});
const r2 = Far('B', {});
compareKeys(r1, r2);            // NaN (different remotables)

Why partial order? Not all Keys can be compared. For example, different remotables have no defined ordering, and CopySets use subset relationships.

Key, Pattern, and Passable Hierarchy

Understanding the type hierarchy:

Passable (everything that can pass)
├── Error
├── Promise
├── Key (stable, comparable)
│   ├── Primitives (null, undefined, boolean, number, bigint, string, symbol)
│   ├── Remotable
│   ├── CopyArray<Key>
│   ├── CopyRecord<Key>
│   ├── CopySet<Key>
│   ├── CopyBag<Key>
│   └── CopyMap<Key, Passable>
└── Pattern (describes a set of Passables)
    ├── Key (matches itself)
    └── Key-like with Matcher leaves
  • Passable: Can cross vat boundaries (from @endo/pass-style)
  • Key: Stable and comparable subset of Passable
  • Pattern: Describes a subset of Passables for matching

Integration with Endo Packages

Complete Tutorial: See Message Passing for a comprehensive guide showing how patterns work with pass-style, exo, and eventual-send.

Deep Dives

For implementation details:

版本与发布记录

@endo/patterns 共发布 30 个版本,首次发布于 2023/03/08;过去 12 个月发布了 4 个版本。

版本发布日期距今
2.1.0latest2026/08/142 个月前
1.9.12026/06/125 个月前
1.9.02026/04/176 个月前
1.8.02026/02/278 个月前
1.7.02025/07/121.3 年前
1.6.12025/06/181.4 年前

关于 @endo/patterns 的常见问题

@endo/patterns 是什么?

Pattern matching for Passable objects, expressed as Passable data

如何安装 @endo/patterns?

在项目目录运行 npm install @endo/patterns,或使用 pnpm add / yarn add / bun add。

@endo/patterns 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@endo/patterns 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@endo/patterns 可以商用吗?

@endo/patterns 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@endo/patterns 的下载量有多少?

根据 npm 官方下载统计,@endo/patterns 在 2026/10/14 – 2026/11/12 这 30 天内被下载 34,837 次,日均约 1,161 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@endo/patterns 还在维护吗?

最新版本 2.1.0 发布于 2026/08/14(2 个月前);至今共发布 30 个版本,其中过去 12 个月发布了 4 个;首次发布于 2023/03/08。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。