Segment string into grapheme clusters once, up front. Every operation on the result reuses
that work rather than re-segmenting.
The raw string.
Number of grapheme clusters. Mirrors String.prototype.length in graphemes.
Count of grapheme clusters. 0 for the empty string.
Iterate the grapheme clusters, so Array.from(...) and spreading behave the way they do on a
native string — with clusters as the unit. Without this an instance would read as array-like,
and Array.from would silently produce a run of undefined instead of failing.
An iterator over the grapheme clusters, in order.
Mirrors String.prototype.at. The grapheme at index, or undefined if out of bounds.
Negative indexes count back from the end.
Grapheme index. Negative counts back from the end; fractional truncates toward
zero and NaN becomes 0.
The grapheme cluster at index, or undefined when out of bounds.
Mirrors String.prototype.charAt. The grapheme at index, or '' if out of bounds. Like
native — and unlike at — a negative index is out of bounds rather than counted from the
end.
Grapheme index. Fractional truncates toward zero and NaN becomes 0.
The grapheme cluster at index, or '' when out of bounds.
Mirrors String.prototype.codePointAt, indexed by grapheme. For a grapheme built from several
code points this reports only the first one.
Grapheme index. Fractional truncates toward zero and NaN becomes 0.
The first code point of the grapheme at index, or undefined when out of bounds.
Mirrors String.prototype.endsWith: whether an occurrence of searchString ends exactly at
endPosition (default: the end of the string). A negative endPosition clamps to 0 and an
empty needle returns true. The match must begin on a grapheme boundary.
Needle to look for. Used raw and never segmented.
OptionalendPosition: numberGrapheme index the match must end at. Defaults to the end of the string; negative clamps to 0.
true if searchString ends exactly at endPosition and begins on a grapheme
boundary. An empty needle returns true.
formatReplacementToArray with every part coerced to a string and joined.
Map from key text to its replacement. A key absent from the map is replaced by the key text itself.
The formatted string. '' if this string is empty. A replacer that cannot be
converted becomes [object Object], or [object Unknown] if even that inspection throws.
new GraphemeString('a{n}b').formatReplacement({ n: 9000 }); // 'a9000b'
A replacer that cannot be converted to a string degrades to a placeholder rather than throwing, because the template is a localized string and the replacers are caller-supplied values — one bad value must not take down the whole call. Use formatReplacementToArray to keep a non-string replacer intact instead of coerced.
Replace each {key} in this string with replacers[key] and unescape \{/\}. An unknown
key is replaced by the key text itself. Adjacent strings are concatenated, so a replacer that
is not a string stays its own entry — which is how a React element survives being substituted
in.
No native counterpart, but it walks the string character by character, so it belongs here rather than beside the plain string helpers: an instance built once from a template can be formatted repeatedly without re-segmenting it.
Map from key text to its replacement. A key absent from the map is replaced by the key text itself rather than treated as an error.
The formatted parts in order. Adjacent strings are merged into one entry, so a non-string replacer is always its own entry. A template with no placeholders yields a single string entry; only the empty string yields an empty array.
Mirrors String.prototype.includes. See indexOf for position and boundary rules.
Needle to find. Used raw and never segmented.
Optionalposition: numberGrapheme index to start from. Defaults to 0; negative clamps to 0.
true if searchString occurs on grapheme boundaries at or after position. An
empty needle returns true.
Mirrors String.prototype.indexOf: the first grapheme index at or after position where
searchString occurs, or -1. A negative position clamps to 0, and an empty needle reports
the clamped position itself. Only a hit that begins and ends on a grapheme boundary counts,
so searching for a single emoji that forms part of a larger cluster reports -1 rather than
matching inside it.
Accepts a raw string or a GraphemeString; the needle is used raw and is never segmented.
Needle to find. Used raw and never segmented.
Optionalposition: numberGrapheme index to start from. Defaults to 0; negative clamps to 0.
The grapheme index of the first match, or -1 if there is none. An empty needle
returns the clamped position.
Mirrors String.prototype.lastIndexOf: the last grapheme index at or before position where
searchString occurs, or -1. As in native, an omitted or NaN position searches the whole
string while a negative one clamps to 0. See indexOf for the boundary rule.
Needle to find. Used raw and never segmented.
Optionalposition: numberGrapheme index to search at or before. Omitted or NaN searches the whole
string; negative clamps to 0.
The grapheme index of the last match, or -1 if there is none. An empty needle
returns the clamped position.
Mirrors String.prototype.padEnd. See padStart, including the RangeError ceiling and
why this returns text rather than a GraphemeString.
Desired length in graphemes.
OptionalpadString: stringText to repeat. Defaults to a single space.
The padded text, or this instance's text unchanged when no padding is needed.
RangeError when targetLength exceeds MAX_PADDING_LENGTH and padding would
actually be added.
Mirrors String.prototype.padStart, choosing whole graphemes as filler so the result is
targetLength graphemes long — where native fills UTF-16 slots and can leave a broken half of
a cluster at the seam. Throws RangeError above MAX_PADDING_LENGTH, a lower ceiling
than native's and the class's one deliberate departure from native behavior.
Returns text rather than a GraphemeString, unlike the range methods. Those only ever remove
clusters, so a range of an honest segmentation is still honest and can be carried into the
result for free. Padding adds text at a seam, and added text can fuse with what is already
there — a filler ending in a combining mark joins the character it lands against — so there is
no segmentation to carry. Constructing one here would mean either re-segmenting on every call
or handing back an instance whose cluster array disagrees with its own text.
Desired length in graphemes. No padding is added when it is at or below the current length.
OptionalpadString: stringText to repeat, truncated at a grapheme boundary. Defaults to a single space; an empty string adds no padding.
The padded text, or this instance's text unchanged when no padding is needed.
RangeError when targetLength exceeds MAX_PADDING_LENGTH and padding would
actually be added. An empty padString never pads, so it never throws.
Mirrors String.prototype.slice. Negative indexes count back from the end and a backwards
range yields an empty result.
OptionalindexStart: numberFirst grapheme to include. Defaults to 0; negative counts back from the end.
OptionalindexEnd: numberFirst grapheme to exclude. Defaults to the end; negative counts back from the end.
A new instance over [indexStart, indexEnd), reusing this instance's segmentation.
Empty when the range is backwards or empty.
Mirrors String.prototype.split, including the parts that surprise people: a splitLimit
discards everything past the limit rather than keeping it as a final piece, the limit is
converted with ToUint32 (so -1 means "no limit" while NaN and Infinity mean "empty
result"), an omitted separator yields the whole string, and a regular expression's capture
groups are interleaved into the result.
The grapheme substitutions: an empty separator splits into graphemes rather than UTF-16 units, and a separator only matches where it begins and ends on grapheme boundaries.
PERF: an empty separator wraps every grapheme in its own instance. When the text is all that is wanted, toArray produces the same clusters as plain strings and skips that entirely.
Entries are undefined exactly where native produces undefined — a capture group that did
not participate in the match.
Optionalseparator: string | GraphemeStringLiteral string to split on, raw or as a GraphemeString. Omitted yields the
whole string as a single piece; '' splits into individual graphemes.
OptionalsplitLimit: numberMaximum number of entries to return, converted with ToUint32. Anything past
the limit is discarded rather than kept as a final piece. Omitted means no limit.
The pieces in order. Empty when splitLimit resolves to 0. Never contains undefined
— only a capture group can produce one, and a literal separator has none.
Splitting on a regular expression. See the string overload for the shared rules.
Regular expression to split on. Its capture groups are interleaved into the result.
OptionalsplitLimit: numberMaximum number of entries to return, converted with ToUint32.
The pieces in order. An entry is undefined exactly where a capture group did not
participate in its match, as native does.
Splitting on a separator whose kind is not known statically — a string | RegExp union, which
is what the free split in string-util declares. TypeScript matches a union argument against
one overload at a time rather than distributing it, so without this a caller holding that union
gets TS2769 and has to narrow at every call.
Literal string, GraphemeString, or regular expression to split on.
OptionalsplitLimit: numberMaximum number of entries to return, converted with ToUint32.
The pieces in order. An entry can be undefined only when the separator turns out to
be a regular expression with a capture group that did not participate.
Mirrors String.prototype.startsWith: whether an occurrence of searchString begins at
position. A negative position clamps to 0 and an empty needle returns true. The match
must end on a grapheme boundary, so a prefix ending mid-cluster is rejected.
Needle to look for. Used raw and never segmented.
Optionalposition: numberGrapheme index the match must begin at. Defaults to 0; negative clamps to 0.
true if searchString begins at position and ends on a grapheme boundary. An
empty needle returns true.
Mirrors String.prototype.substring. Negative indexes clamp to 0 rather than counting from the
end, and — as in native — the arguments are swapped when begin is greater than end.
Optionalbegin: numberFirst grapheme to include. Defaults to 0; negative clamps to 0.
Optionalend: numberFirst grapheme to exclude. Defaults to the end; negative clamps to 0.
A new instance over the range, reusing this instance's segmentation. Empty when
begin and end resolve to the same index.
The grapheme clusters as an array. Returns a fresh copy, so mutating it cannot corrupt this instance. Equivalent to spreading this instance, and to spreading a native string except that native yields code points rather than clusters.
A new array of the grapheme clusters, in order. Empty for the empty string.
The text, for JSON.stringify. Without this an instance would serialize its internals — the
raw string and the grapheme array beside it — at roughly twice the size of the text, and the
result would not read back as anything useful.
Note that this makes serialization one-way: what comes off the wire is a plain string, not a
GraphemeString. The class is a local segmentation cache rather than a transferable value, so
a receiver that wants one constructs it from the text.
The raw string this instance was built from, unchanged.
The original raw string. Named toString rather than exposed as a property so an instance
drops straight into a template literal or String(...) without an accessor.
The raw string this instance was built from, unchanged.
A string pre-segmented into Unicode grapheme clusters. Segmentation happens once in the constructor (the expensive step); every other operation reuses it. Derived values (substring/slice/etc.) reuse the parent grapheme slice rather than re-segmenting. The single exception is a regular expression's capture groups, whose text the parent's segmentation does not cover — a group can match across a boundary the parent never had.
Every method mirrors its
String.prototypecounterpart exactly — including the edge cases around negative, fractional,NaN, and out-of-range arguments — with one substitution: the unit of indexing and length is the grapheme cluster rather than the UTF-16 code unit. Solengthcounts what a reader would call characters,slicenever cuts a cluster in half, and a search only reports a hit that begins and ends on cluster boundaries.The surface is limited to operations that actually need the segmentation: toArray and formatReplacement have no native counterpart but do need it, while
normalizeandordinalCompareare deliberately absent, because neither reads the string as characters — they live with the plain string helpers instring-utilinstead.Range methods return a
GraphemeStringrather than astringso the parent's segmentation carries into the result instead of being recomputed. CalltoString()for the text. The padding methods return text instead, because they are the one pair that adds characters: added text can fuse with the text it lands against, so there is no segmentation to carry.Segmentation fidelity
Clusters come from
unicode-segmenter, which implements UAX #29 extended grapheme clusters against Unicode 17.lengthcounts what a literate reader of the script would call characters — for emoji and Latin, and equally for pointed Hebrew, vocalized Arabic and Syriac, Indic conjuncts, Thai, and Hangul written as decomposed Jamo.Two consequences deserve attention, because they are where a conformant segmenter changes an answer a caller might be relying on.
\r\nis a single cluster (rule GB3). Since searches only report boundary-aligned hits,'\n'is therefore NOT findable inside a\r\n, and split on'\n'will not break Windows-style lines apart. Split lines with a regex that matches the whole terminator (/\r?\n/) rather than the bare line feed.A zero-width joiner attaches to the character before it (rule GB9), not the one after. So
'a\u200d 'is two clusters —'a\u200d'and a space — and that trailing space is a cluster that is entirely whitespace.Example