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, leven 4.1.0, cspell-trie-lib 10.2.1, and @nlptools/distance 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.
| Capability | string-typo-match | leven | cspell-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 units | Mixed: 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 rules, plus swaps and repeated-letter discounts in its trie search. Its suggestion options 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 collection 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 argument | Type | Obligatory | Description |
|---|---|---|---|
inputType: String Obligatory: yes | |||
input | String | yes | The observed text, including any typing errors. |
candidatesType: Array of strings Obligatory: yes | |||
candidates | Array of strings | yes | Intended strings to consider. Entries must be nonempty. An empty array is valid. |
optionsType: Plain object Obligatory: no | |||
options | Plain object | no | Scoring 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
status | Meaning | bestMatch |
|---|---|---|
exact | ||
exact | The literal input is in the vocabulary. Only that candidate is returned, with cost zero. | The exact string. |
matched | ||
matched | The cheapest eligible candidate is unique and has the required lead over the next candidate. | The selected string. |
ambiguous | ||
ambiguous | Candidates tie for minimum cost, or their cost gap is too small. | null. |
no-match | ||
no-match | No 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.
| Option | Default | Description |
|---|---|---|
maxEventsDefault: 1 | ||
maxEvents | 1 | Maximum local error events. Supports 1 or 2. |
maxCostDefault: 200 | ||
maxCost | 200 | Inclusive maximum total cost. Zero permits only exact matches. |
minCostGapDefault: 25 | ||
minCostGap | 25 | Required cost lead over the next eligible candidate. Exact cost ties remain ambiguous even at zero. |
minInputLengthDefault: 3 | ||
minInputLength | 3 | Minimum observed length, in Unicode code points, for fuzzy matching. |
maxOmissionLengthDefault: 4 | ||
maxOmissionLength | 4 | Maximum code points in one omission run. One permits single omissions only. |
maxOmissionRatioDefault: 0.5 | ||
maxOmissionRatio | 0.5 | Maximum fraction of candidate code points omitted across the entire explanation. Zero disables omissions. |
keyboardDefault: null | ||
keyboard | null | Disable keyboard weighting, select "qwerty", or supply a directed adjacency map. |
costsDefault: See below | ||
costs | See below | Override 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 key | Default | Candidate → input event |
|---|---|---|
missingCharacterDefault: 100 | ||
missingCharacter | 100 | One code point is missing. |
omissionOpenDefault: 100 | ||
omissionOpen | 100 | Initial cost of a contiguous omitted block. |
omissionExtendDefault: 20 | ||
omissionExtend | 20 | Additional cost for each omitted code point after the first. |
extraCharacterDefault: 100 | ||
extraCharacter | 100 | One extra input code point; also applies when cheaper than repetition weighting. |
repeatedCharacterDefault: 75 | ||
repeatedCharacter | 75 | One extra copy beside an unchanged retained neighbor. |
adjacentSwapDefault: 100 | ||
adjacentSwap | 100 | Two adjacent, distinct code points exchange positions. |
keyboardSubstitutionDefault: 75 | ||
keyboardSubstitution | 75 | One code point becomes a declared neighboring key. |
substitutionDefault: 150 | ||
substitution | 150 | One 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 reports 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";