Skip to Content
  • Website
Codsen
  • Home
  • Open Source
  • Articles
  • About

prevOpen Source→string-typo-matchnext

string-typo-match1.1.0

Match typing errors against candidate strings with weighted omissions, swaps, and explainable ambiguity

Downloads per monthChangelogMIT Licenselibera manifesto
  • the top
  • Installation
  • Quick Take
  • Examples
  • PURPOSE
  • FEATURE COMP…
  • API — MATCHTYPOS()
  • API — CREATEMATCHE…
  • RESULTS AND…
  • OPTIONS
  • COSTS
  • EXPLANATIONS…
  • KEYBOARD ADJ…
  • PROGRESS AND…
  • PERFORMANCE
  • API — DEFAULTS
  • API — VERSION
  • API — TYPES
  • Changelog

No dependencies whatsoever. This package declares no dependencies or devDependencies.

Permalink to InstallationInstallation

Permalink to Quick TakeQuick Take

Permalink to ExamplesExamples

  • Preserve ambiguous suggestions
  • Interpret original UTF-16 spans
  • Supply directional keyboard neighbours
  • Configure long omissions explicitly
  • Reuse a candidate vocabulary
  • Observe preparation and lookup progress

Purpose

Match an observed string against a vocabulary of intended strings, allowing plausible typing errors. The result explains what changed and leaves competing suggestions ambiguous.

import { matchTypos } from "string-typo-match";

const result = matchTypos("astrisk", ["asterisk", "attribute", "assertion"]);

console.log(result.status);
// => "matched"
console.log(result.bestMatch);
// => "asterisk"
console.log(result.matches[0].operations[0].kind);
// => "missing-character"

A contiguous omission counts as one error event. For example, Levenstein → Lenstein loses two adjacent letters, while abcdef → acef loses two letters in separate places and needs two events. Limits on omission length and proportion prevent one event from excusing an arbitrarily large loss.

Use this library for entity names, commands, identifiers, and other finite vocabularies. You supply the candidates; it has no runtime dependencies, built-in dictionary, or automatic text replacement.

Feature comparison

For commands, entity names, and other known vocabularies, string-typo-match combines human-typo rules, bounded omissions, and an explained decision in one API.

Compared on 5 September 2026: string-typo-match 1.0.0, levenopens in a new tab 4.1.0, cspell-trie-libopens in a new tab 10.2.1, and @nlptools/distanceopens in a new tab 0.0.8. The table covers those packages’ public APIs, including their search helpers; it does not compare the full CSpell application.

✅ Yes means the package provides the capability, sometimes through configuration or a separate export. ❌ No means no equivalent built-in capability; callers could add their own logic. A qualified cell describes the difference.

Capabilitystring-typo-matchlevencspell-trie-lib@nlptools/distance
Search your own vocabulary
Search your own vocabulary✅ Yes✅ Yes✅ Yes✅ Yes
Prepare a vocabulary for repeated lookups
Prepare a vocabulary for repeated lookups✅ Yes❌ No✅ Trie✅ Search collection
Configure edit weights
Configure edit weights✅ Yes❌ No✅ Yes✅ Alignment scores
Count an adjacent swap as one edit
Count an adjacent swap as one edit✅ Yes❌ No✅ Yes✅ Separate algorithm
Discount repeated letters
Discount repeated letters✅ Yes❌ No✅ Trie search❌ No
Weight keyboard-neighbor mistakes
Weight keyboard-neighbor mistakes✅ Opt-in❌ No✅ Custom rules❌ No
Select a built-in QWERTY preset
Select a built-in QWERTY preset✅ Opt-in❌ No❌ Supply layout❌ No
Score any omitted block with opening and extension costs
Score any omitted block with opening and extension costs✅ Yes❌ No❌ Specific blocks only✅ Affine alignment
Limit typo events separately from total cost
Limit typo events separately from total cost✅ One or two❌ No❌ Cost budget❌ No
Cap each omitted block’s length
Cap each omitted block’s length✅ Yes❌ No❌ No❌ No
Cap the total omitted fraction of a candidate
Cap the total omitted fraction of a candidate✅ Yes❌ No❌ No❌ No
Return every eligible fuzzy candidate, ranked
Return every eligible fuzzy candidate, ranked✅ Yes❌ One result❌ Bounded search✅ Linear search
Report ambiguity with a configurable winning margin
Report ambiguity with a configurable winning margin✅ Yes❌ No❌ Caller logic❌ Caller logic
Return weighted typo operations with original-string spans
Return weighted typo operations with original-string spans✅ Yes❌ No❌ No❌ No
Use Unicode code points for edit-based matching
Use Unicode code points for edit-based matching✅ Yes❌ UTF-16 unitsMixed: trie traversal❌ Edit algorithms use UTF-16
Report percentage progress and completion statistics
Report percentage progress and completion statistics✅ Yes❌ No❌ No equivalent API❌ No
Include TypeScript declarations
Include TypeScript declarations✅ Yes✅ Yes✅ Yes✅ Yes
Install without dependency or required peer packages
Install without dependency or required peer packages✅ Yes✅ Yes❌ Required peer❌ Three dependencies
Load a packaged standalone browser script
Load a packaged standalone browser script✅ Yes❌ No❌ No❌ No

The keyboard rule is optional here: enable "qwerty" or supply a directed map. Our omission policy treats a contiguous loss as one event while charging for its length; it also limits how much of the candidate can disappear. These rules work together with swaps, repetition, ambiguity reporting, and edit spans in the same result.

CSpell already supports keyboard weighting through cost maps and Hunspell keyboard rulesopens in a new tab, plus swaps and repeated-letter discounts in its trie searchopens in a new tab. Its suggestion optionsopens in a new tab control result counts, ties, and timeouts. These do not provide this library’s complete fuzzy suggestion list and explicit ambiguity decisions. Trie traversal uses code points; scalar weighted distance and weighted scoring of trie suggestions use UTF-16 units.

The distance collectionopens in a new tab offers affine alignment, a separate swap-aware distance, and reusable fuzzy search. Its unlimited linear search can return every result above its similarity threshold; enabling its optional approximate index can miss matches. Those algorithms do not combine into our bounded typo-event model automatically. Its separate diff and common-subsequence tools also serve different purposes from weighted typo-operation spans.

Choose string-typo-match when you need to explain a proposed repair and decline uncertain ones. It supports Node.js 18.20.8 and later, plus a direct-browser bundle targeting Chromium 58. For scalar distance, broader dictionary tooling, or general sequence alignment, the alternatives remain useful. Throughput depends on the workload; see Performance.

API — matchTypos()

The main function matchTypos() is imported like this:

Input argumentTypeObligatoryDescription
input
Type: String
Obligatory: yes
inputStringyesThe observed text, including any typing errors.
candidates
Type: Array of strings
Obligatory: yes
candidatesArray of stringsyesIntended strings to consider. Entries must be nonempty. An empty array is valid.
options
Type: Plain object
Obligatory: no
optionsPlain objectnoScoring policy and an optional progress callback.

The function returns a MatchResult synchronously. It does not mutate your inputs. Comparison is literal and case-sensitive: there is no trimming, case folding, punctuation removal, or Unicode normalization.

Exact duplicates in candidates are considered once and retain their first original index. An empty observed input returns no-match.

API — createMatcher()

Prepare once when you will reuse a vocabulary:

The function createMatcher() is imported like this:

import { createMatcher } from "string-typo-match";

const matcher = createMatcher(["screen", "print", "speech"]);

console.log(matcher.match("scren").bestMatch);
// => "screen"
console.log(matcher.match("prnit").bestMatch);
// => "print"

The matcher snapshots the candidates and scoring options. Later changes to your arrays or keyboard map do not affect it. Its match(input, options?) method accepts only a per-call progressFn; create another matcher to change scoring policy.

Results and ambiguity

statusMeaningbestMatch
exact
exactThe literal input is in the vocabulary. Only that candidate is returned, with cost zero.The exact string.
matched
matchedThe cheapest eligible candidate is unique and has the required lead over the next candidate.The selected string.
ambiguous
ambiguousCandidates tie for minimum cost, or their cost gap is too small.null.
no-match
no-matchNo candidate is eligible under the policy.null.

For example, a missing letter can produce nsp from three different entity names:

const result = matchTypos("nsp", ["ensp", "nbsp", "nsup"]);

console.log(result.status);
// => "ambiguous"
console.log(result.bestMatch);
// => null
console.log(result.matches.map(({ candidate }) => candidate));
// => ["ensp", "nbsp", "nsup"]

For fuzzy matches, every eligible candidate is returned. Suggestions are ordered by cost, then case-sensitive JavaScript string ordering. Candidate input order cannot resolve a cost tie. Duplicate candidates do not manufacture ambiguity.

An exact input always wins: not stays not when both not and notin are candidates. Exact matching works at every nonempty length, even below the minimum length for fuzzy matching.

Options

Pass any subset of the policy fields. Nested costs overrides merge with the default costs.

OptionDefaultDescription
maxEvents
Default: 1
maxEvents1Maximum local error events. Supports 1 or 2.
maxCost
Default: 200
maxCost200Inclusive maximum total cost. Zero permits only exact matches.
minCostGap
Default: 25
minCostGap25Required cost lead over the next eligible candidate. Exact cost ties remain ambiguous even at zero.
minInputLength
Default: 3
minInputLength3Minimum observed length, in Unicode code points, for fuzzy matching.
maxOmissionLength
Default: 4
maxOmissionLength4Maximum code points in one omission run. One permits single omissions only.
maxOmissionRatio
Default: 0.5
maxOmissionRatio0.5Maximum fraction of candidate code points omitted across the entire explanation. Zero disables omissions.
keyboard
Default: null
keyboardnullDisable keyboard weighting, select "qwerty", or supply a directed adjacency map.
costs
Default: See below
costsSee belowOverride individual event weights.

Event count and cost are independent limits. Two low-cost events cannot pass a one-event policy. For example:

console.log(matchTypos("acef", ["abcdef"]).status);
// => "no-match"

const result = matchTypos("acef", ["abcdef"], { maxEvents: 2 });
console.log(result.matches[0].eventCount);
// => 2

Two separate omissions share the same ratio allowance. A contiguous omission cannot be split into adjacent events to evade its length limit. Empty input never produces a whole-candidate omission suggestion.

maxCost and minCostGap must be nonnegative safe integers. Length limits must be positive safe integers. The omission ratio must be a finite number from zero to one. Unknown keys and invalid values throw numbered input-validation errors.

Costs

Lower cost means a stronger match under your policy. Costs are engineering weights, not probabilities or confidence percentages.

Cost keyDefaultCandidate → input event
missingCharacter
Default: 100
missingCharacter100One code point is missing.
omissionOpen
Default: 100
omissionOpen100Initial cost of a contiguous omitted block.
omissionExtend
Default: 20
omissionExtend20Additional cost for each omitted code point after the first.
extraCharacter
Default: 100
extraCharacter100One extra input code point; also applies when cheaper than repetition weighting.
repeatedCharacter
Default: 75
repeatedCharacter75One extra copy beside an unchanged retained neighbor.
adjacentSwap
Default: 100
adjacentSwap100Two adjacent, distinct code points exchange positions.
keyboardSubstitution
Default: 75
keyboardSubstitution75One code point becomes a declared neighboring key.
substitution
Default: 150
substitution150One code point is replaced without a cheaper applicable rule.

A single omission uses missingCharacter. A block of length k, where k is at least two, costs omissionOpen + (k - 1) * omissionExtend.

const result = matchTypos("nbssp", ["nbsp"], {
  costs: { repeatedCharacter: 60 },
});

console.log(result.matches[0].cost);
// => 60

All event costs must be positive safe integers. omissionExtend may be zero. Configuration is rejected if a block could cost less than a single omission or its configured cost could exceed safe integer bounds.

Explanations and indexes

Explanations describe intended candidate → observed input:

operations appear in alignment order. Their costs sum to the candidate’s cost, and their count equals eventCount. The operation kinds are missing-character, omitted-block, repeated-character, extra-character, adjacent-swap, keyboard-substitution, and substitution.

The candidateFrom/candidateTo and inputFrom/inputTo pairs are half-open UTF-16 offsets into the original strings, suitable for slice(from, to). Matching and length limits use Unicode code points, so an astral symbol is one character but occupies two offsets:

const result = matchTypos("ab", ["a😀b"], { minInputLength: 1 });
console.log(result.matches[0].operations[0]);
// => {
//      kind: "missing-character",
//      candidateFrom: 1,
//      candidateTo: 3,
//      inputFrom: 1,
//      inputTo: 1,
//      cost: 100
//    }

A missing segment has an empty input span. An extra segment has an empty candidate span. Unchanged spans between operations, together with the operations’ input spans, reproduce the observed string. Lone surrogates are preserved literally; code-point comparison does not imply grapheme-cluster or linguistic equivalence.

Equal-cost explanations for one candidate prefer fewer events, then the leftmost operation sequence. Thus Levenstein → Lenstein canonically omits ev at candidate [1,3). Omitting ve at [2,4) produces the same input but is a later alignment. Multiple explanations of one candidate do not cause candidate ambiguity.

Events are local and non-overlapping. A swap cannot reuse characters from another event. A character used in a swap or substitution cannot qualify as the unchanged neighbor for a repetition. The model does not allow an insertion between swapped characters: CA → ABC is excluded even at two events.

Keyboard adjacency

Keyboard weighting is disabled by default. Supply an explicit directed map when you know which key confusions to favor:

const result = matchTypos("rest", ["test"], {
  keyboard: { t: ["r"] },
});

console.log(result.matches[0].operations[0].kind);
// => "keyboard-substitution"
console.log(result.matches[0].cost);
// => 75

The map permits intended t to be observed as r; it does not infer the reverse edge. Each key and neighbor must be exactly one code point. A more expensive keyboard specialization loses to the general substitution cost.

The "qwerty" preset uses fixed US-QWERTY letter adjacency. Row offsets are 0, 0.25, and 0.75, and key centers at most 1.3 spacings apart are neighbors. Uppercase neighbors stay uppercase. Digits, punctuation, cross-case edges, and other layouts are excluded and receive general substitution costs.

Binary adjacency assigns the same cost to all declared neighbors. With { o: ["p"], a: ["w"] }, observed pwned ties between owned and paned. The library has no hidden word-frequency ranking or initial-letter discount. A valid exact candidate still wins.

Progress and completion statistics

Both public functions accept progressFn, and prepared lookups accept a separate per-call callback:

const preparation = [];
const matching = [];
const matcher = createMatcher(["screen", "print", "speech"], {
  progressFn: (percentage) => preparation.push(percentage),
});
const result = matcher.match("scren", {
  progressFn: (percentage) => matching.push(percentage),
});

console.log(result.bestMatch);
// => "screen"
console.log(matching[0], matching[matching.length - 1]);
// => 0 100

Successful callback sequences start at 0, end at 100, increase monotonically, and report each integer percentage at most once. The final callback follows scoring, explanation construction, and ordering. matchTypos() reports the complete preparation-and-lookup operation. Callbacks are synchronous and do not yield to browser painting. A callback exception propagates without corrupting a prepared matcher for its next call.

matcher.log reports candidateCount, uniqueCandidateCount, and timeTakenInMilliseconds. Each result’s log reports uniqueCandidateCount, evaluatedCandidateCount, prunedCandidateCount, eligibleCandidateCount, and timeTakenInMilliseconds.

Evaluated candidates entered approximate scoring; pruned candidates failed a sound bound. Deduplication and exact-lookup bypasses count as neither. Elapsed milliseconds are nonnegative, best-effort measurements and do not affect decisions. One-shot timing includes preparation; prepared lookup timing covers that lookup.

Results contain plain JSON-compatible data and can cross postMessage(). A prepared matcher contains a function and is not transferable.

Performance

Prepare a matcher when you will reuse its vocabulary. Preparation snapshots the strings, builds exact lookup, and stores code-point offsets; a lookup then scans candidates that pass sound bounds. Two-event matching searches more alignments than the default one-event policy.

This library returns weighted decisions and explanations, while leven returns a scalar edit distance. They can accept different repairs. Choose between them by the behavior your application needs, and measure your own vocabulary, policy, and call pattern.

Benchmarks using HTML entity names, media descriptors, and repository spelling errors found mixed results. Reusing the default one-event matcher improved throughput on some vocabularies; one-shot and two-event matching could be slower than a capped leven search. The evaluation scripts and reportsopens in a new tab record the samples, policies, and repeated measurements.

API — defaults

You can import defaults:

It's a plain object:

The main function calculates the options to be used by merging the options you passed with these defaults.

The exported object and its nested costs are frozen at runtime and read-only in TypeScript. Pass overrides to a function instead of modifying the defaults.

API — version

You can import version:

API — types

The declarations are generated from TypeScript source. The package exports MatchOptions, CallOptions, TypoCosts, Keyboard, Matcher, PreparationLog, MatchLog, MatchResult, CandidateMatch, TypoOperation, and TypoKind.

import type { MatchOptions, MatchResult, TypoOperation } from "string-typo-match";

Permalink to changelogChangelog

Open Changelog
↑ back to top
prev next

Copyright

All rights reserved © Roy Revelt 2026
All our open source packages are under MIT licenceopens in a new tab

Activities

🐛 See a bug? Raise an issueopens in a new tab
💘 Check out the Indiewebopens in a new tab and Libera manifestoopens in a new tab