Skip to content

feat: add matches and isMatch functions#383

Open
boy672820 wants to merge 6 commits intomarpple:mainfrom
boy672820:feat/matches
Open

feat: add matches and isMatch functions#383
boy672820 wants to merge 6 commits intomarpple:mainfrom
boy672820:feat/matches

Conversation

@boy672820
Copy link
Copy Markdown

Fixes #380

Summary

  • Add matches function - creates a predicate that checks if an input matches all properties in a pattern
  • Add isMatch function - performs partial deep comparison between two values

Features

  • Deep comparison for nested objects and arrays
  • Support for Date, RegExp, Map, Set
  • Works with filter, find, some, every

@boy672820 boy672820 requested a review from ppeeou as a code owner February 16, 2026 16:56
Copy link
Copy Markdown
Member

@ppeeou ppeeou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your interest in FxTS!

Here are a few comments after reviewing the code.


1. isMatch — Map/Set partial matching inconsistency

isMatch is documented as a "partial deep comparison", and objects do behave as partial matches:

isMatch({ a: 1, b: 2 }, { a: 1 }); // true — OK if source is a subset

However, Map and Set require an exact size match:

// src/isMatch.ts
if (source instanceof Map && object instanceof Map) {
    if (source.size !== object.size) return false;  // ← not partial

isMatch(new Map([['a',1],['b',2]]), new Map([['a',1]]))false, but the same data represented as a plain object would return true. Consider either applying partial matching semantics consistently, or explicitly documenting that Map/Set use exact match.


2. matches type signature — T always resolves to unknown

function matches<T>(pattern: Record<Key, any>): (input: T) => boolean;

T is never inferred from the pattern, so matches({ age: 30 }) always returns (input: unknown) => boolean. There is no type checking on pattern keys, meaning non-existent properties pass silently:

// No type error — age2 doesn't exist on User, but compiles fine
filter(matches({ age2: 30 }), users);

Consider leveraging Partial<T> or similar approaches to provide type safety for pattern keys.


3. isMatch Set comparison — greedy matching

When comparing Sets, each source value is searched against all object values, but already-matched object values can be matched again:

for (const value of source) {
  let found = false;
  for (const objValue of object) {  // ← already matched objValue can be matched again
    if (isMatch(objValue, value)) {
      found = true;
      break;
    }
  }
}

Currently the exact size check mitigates this, but if issue #1 is addressed by switching to partial matching, this would become a bug. Consider tracking matched values (e.g., using a matched Set).


4. entries only returns string keys

The pattern type for matches is Record<Key, any> where Key = string | symbol | number, but entries(pattern) is based on Object.entries, which ignores symbol keys.

const sym = Symbol('id');
matches({ [sym]: 123 }); // symbol key is silently ignored during comparison

Since the Key type includes symbol, consider either documenting this limitation or excluding symbol from the accepted key type.

- SameValueZero for primitives
- Type guards for Date/RegExp/Map/Set
- DFS for Set matching
- Array partial match
…ymbolKeys

- Use DeepPartial<T> & RejectSymbolKeys<T> for partial matching and Symbol rejection
- Add DeepWiden<T> to widen literals to base types
@boy672820
Copy link
Copy Markdown
Author

boy672820 commented Feb 23, 2026

Thank you for the review!

Issue #1: Map/Set partial matching inconsistency

As you noted, requiring an exact size match with source.size !== object.size for Map and Set was inconsistent with the "partial deep comparison" documentation.
This change applies partial matching semantics to both Map and Set:

  • Map: Returns false only when source.size > object.size. Returns true if all keys in source exist in object with matching values.
  • Set: Returns false only when source.size > object.size. Returns true if all values in source are matched within object.
  • Array: Partial matching added on top of the existing order-based comparison. Returns false only when source.length > object.length, true if source matches the prefix of object.
isMatch(new Map([['a',1],['b',2]]), new Map([['a',1]])); // true
isMatch(new Set([1, 2, 3]), new Set([1, 2])); // true
isMatch([1, 2, 3], [1, 2]); // true

Since a Set is a collection of unique values, partial matching is performed regardless of order. Arrays, on the other hand, are order-significant, so they are compared index by index.


Issue #2: matches type signature T always resolves to unknown

The type signature of matches has been improved as follows:

function matches<T>(
  pattern: DeepPartial<T> & RejectSymbolKeys<T>,
): (input: DeepWiden<T>) => boolean;
  • DeepPartial<T>: Ensures at the type level that pattern is a subset of T. Date, RegExp, Map, and Set pass through without partial application.
  • DeepWiden<T>: Prevents T from being inferred as a literal type (e.g., { age: 30 }) in matches({ age: 30 }), which would result in (input: { age: 30 }) => boolean. Widens boolean, number, and string literals to their respective primitive types.
  • RejectSymbolKeys<T>: As a solution to Issue #4, blocks patterns containing Symbol keys at compile time.

Non-existent property keys now produce type errors:

filter(matches({ age2: 30 }), users);
//                         ~~~~ Error: 'age2' does not exist on type ...

Issue #3: Set comparison greedy matching bug

The greedy matching issue has been fixed using a bipartite matching approach.
In the previous code, an already-matched object value could be matched again by another source value. The new implementation tracks matching state via an assignedSource array and searches for augmenting paths to ensure each object value is matched to at most one source value.

// Before: greedy -> single { a: 1 } matched both source values, returning true (incorrect)
// After: bipartite matching -> correctly returns false
const object = new Set([{ a: 1 }, { b: 2 }]);
const source = new Set([{ a: 1 }, { a: 1 }]);
isMatch(object, source); // false

Matching works correctly regardless of iteration order:

const object = new Set([{ a: 1, b: 1 }, { a: 1 }]);
const source = new Set([{ a: 1 }, { a: 1, b: 1 }]);
isMatch(object, source); // true

Issue #4: entries only returns string keys (Symbol keys silently ignored)

Since matches uses entries() internally, Symbol keys are silently ignored at runtime. Rather than changing this behavior, the fix rejects Symbol keys at the type level:

type RejectSymbolKeys<T> = T extends Date | RegExp | Map<any, any> | Set<any>
  ? T
  : T extends object
  ? { [K in keyof T]: K extends symbol ? never : ... }
  : T;

Passing a pattern with Symbol keys now produces a compile-time error:

const sym = Symbol("id");
matches({ [sym]: 123 });
//.               ~~~~ Error: Type 'number' is not assignable to type 'never'

This behavior has also been explicitly documented in the JSDoc.

Copy link
Copy Markdown
Member

@ppeeou ppeeou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are incorrect comments about array matching behavior in three places. The behavior was changed to prefix-based partial matching, but some comments still say "must match exactly", which contradicts the examples immediately below them.


1. src/isMatch.ts — JSDoc comment (source of truth)

// Array matching (must match exactly)   ← incorrect
isMatch([1, 2, 3], [1, 2, 3]); // true
isMatch([1, 2, 3], [1, 2]); // true      ← contradicts the comment above
isMatch([1, 2], [1, 2, 3]); // false

Should be something like:

// Array matching (partial, prefix-based)

2. website/docs/ja/api/isMatch.md — inherited the same wrong comment

// 配列マッチング(完全に一致する必要がある)  ← incorrect ("must match exactly")
isMatch([1, 2, 3], [1, 2]); // true             ← contradicts it

The ko version (// 배열 매칭) was already updated correctly.


3. website/docs/zh/api/isMatch.md — same issue

// 数组匹配(必须完全匹配)  ← incorrect ("must match completely")
isMatch([1, 2, 3], [1, 2]); // true  ← contradicts it

4. website/docs/zh/api/isMatch.md and matches.md — missing auto-generated header

Both zh files are missing <!-- Do not edit this file. It is generated automatically -->, which is present in the ja and ko equivalents.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add matches function for object pattern matching

2 participants