truncateSafe
Truncates a string by character length without breaking words. Appends an ellipsis (or custom suffix) if truncation occurs.
1/**
2 * Truncates a string by character length without breaking words.
3 * Appends an ellipsis (or custom suffix) if truncation occurs.
4 *
5 * @param str - The input string.
6 * @param maxChars - The maximum allowed number of characters (excluding suffix).
7 * @param suffix - Optional suffix to append if truncated (default is '...').
8 * @returns Truncated string without breaking words.
9 */
10export function truncateSafe(
11 str: string,
12 maxChars: number,
13 suffix: string = '...'
14): string {
15 if (maxChars < 0) throw new Error('maxChars must be non-negative');
16
17 const words = str.trim().split(/\s+/);
18 let result = '';
19 let lengthSoFar = 0;
20
21 for (const word of words) {
22 const wordWithSpace = (result ? ' ' : '') + word;
23 if (lengthSoFar + wordWithSpace.length > maxChars) break;
24 result += wordWithSpace;
25 lengthSoFar += wordWithSpace.length;
26 }
27
28 return result.length < str.trim().length ? result + suffix : str;
29}
Prevents Mid-Word Breaks
Ensures no words are split during truncation, improving text readability and aesthetics.
Character-Aware Logic
Respects a strict character limit while preserving whole words, useful for UI constraints.
Customizable Suffix
Supports a custom trailing string (e.g.,
'...'
,' [more]'
), allowing contextual customization.Whitespace Robustness
Handles leading, trailing, and excess intermediate whitespace gracefully with
.trim()
and regex splitting.Fail-Safe Guard
Throws a clear error for negative input, avoiding unexpected behavior.
Tests | Examples
1test('truncateSafe - no truncation needed', () => {
2 expect(truncateSafe('Hello world', 20)).toBe('Hello world');
3});
4
5test('truncateSafe - truncates without breaking word', () => {
6 expect(truncateSafe('The quick brown fox jumps', 15)).toBe('The quick...');
7});
8
9test('truncateSafe - breaks before first word', () => {
10 expect(truncateSafe('Hello', 2)).toBe('...');
11});
12
13test('truncateSafe - exact cutoff', () => {
14 expect(truncateSafe('One two three', 11)).toBe('One two three');
15});
16
17test('truncateSafe - custom suffix', () => {
18 expect(truncateSafe('Longer sentence here', 10, ' [more]')).toBe('Longer [more]');
19});
20
21test('truncateSafe - empty string', () => {
22 expect(truncateSafe('', 5)).toBe('');
23});
24
25test('truncateSafe - zero maxChars', () => {
26 expect(truncateSafe('Some text', 0)).toBe('...');
27});
28
29test('truncateSafe - throws on negative input', () => {
30 expect(() => truncateSafe('Text', -1)).toThrow('maxChars must be non-negative');
31});
Common Use Cases
Product or Article Previews
Display a concise snippet with whole words up to a visual/character limit.
Meta Descriptions & SEO Tags
Format dynamic content to fit meta tag limits (e.g., 150–160 characters) without word breaks.
UX Text Constraints
Useful in UI components where content must stay within strict pixel or character boundaries (e.g., cards, tables).
Push Notification Bodies
Truncate long messages for previews without making them look abruptly cut.
CMS or Blog Editors
Dynamically preview truncated summaries with clean word boundaries for editors or authors.