You can get string between 2 characters in TypeScript with indexOf() and slice(): find the start character, search for the end character from just after it, and slice the text in between. Here’s the core of it:
const from = text.indexOf("[") + 1;
const between = text.slice(from, text.indexOf("]", from));
Nothing below is typed from memory: I saved each example as a .ts file, compiled it in strict mode and ran it on TypeScript 7.0.2 and Node.js v22.22.2.
We’ll cover same and different delimiters, a safe helper that returns undefined, multi-character delimiters, the lazy regex /\[(.*?)\]/, every match with matchAll(), and nested brackets.
Get the string between two characters with indexOf() and slice()
indexOf() returns the position of a character, and slice(start, end) copies everything from start up to, but not including, end. The trick is the second argument of indexOf(), which tells it where to start searching:
const invoice: string = "Invoice [INV-2026-0142] for Austin, TX: $1,249.50";
const openAt = invoice.indexOf("[");
const closeAt = invoice.indexOf("]", openAt + 1);
console.log("[ at", openAt, "and ] at", closeAt);
console.log("between:", invoice.slice(openAt + 1, closeAt));
// different delimiters, and the end one also appears BEFORE the start one
const cookie: string = "city=Austin;state=TX;zip=78701";
const s = cookie.indexOf("state=") + "state=".length;
console.log("wrong:", JSON.stringify(cookie.slice(s, cookie.indexOf(";"))));
console.log("right:", cookie.slice(s, cookie.indexOf(";", s)));
Output:
[ at 8 and ] at 22
between: INV-2026-0142
wrong: ""
right: TX
The cookie line shows why that second argument matters. A plain indexOf(";") finds the semicolon after Austin, which sits before the start, and slice() quietly returns an empty string when the end index is smaller than the start.
If you only need everything after one character, there’s a shorter route in my guide on how to get the string after a character in TypeScript.
What if both delimiters are the same character?
Quotes, pipes and slashes open and close with the same character. Search for the second one starting one position past the first, or you’ll just find the first one again:
const shipping: string = 'Shipped to "Portland, OR" on 2026-10-22';
const q1 = shipping.indexOf('"');
const q2 = shipping.indexOf('"', q1 + 1);
console.log("quotes: ", shipping.slice(q1 + 1, q2));
const row: string = "2026-10-22|Seattle|WA|$84.99";
const firstPipe = row.indexOf("|");
console.log("next pipe:", row.slice(firstPipe + 1, row.indexOf("|", firstPipe + 1)));
console.log("last pipe:", row.slice(firstPipe + 1, row.lastIndexOf("|")));
console.log("split(): ", row.split("|")[1]);
Output:
quotes: Portland, OR
next pipe: Seattle
last pipe: Seattle|WA
split(): Seattle
Notice the difference between “next” and “last”. lastIndexOf() grabs everything up to the final pipe, which is right for a path but wrong for a CSV-style row like this one.
When the delimiter is a separator, split() is often the cleanest answer, as I show in splitting a string and getting the last element. If you want the quotes gone rather than the text inside, see removing quotes from a string in TypeScript.
A safe getBetween() helper for missing delimiters
Real input is messy, and indexOf() returns -1 when a character isn’t there. I wrap the logic in a helper that returns undefined instead of guessing:
function getBetween(text: string, start: string, end: string): string | undefined {
const from = text.indexOf(start);
if (from === -1) return undefined; // start delimiter missing
const begin = from + start.length; // works for "[" and "{{" alike
const to = text.indexOf(end, begin); // look for end AFTER start
if (to === -1) return undefined; // end delimiter missing
return text.slice(begin, to);
}
const samples: string[] = [
"Invoice [INV-2026-0142] for Austin, TX",
"Invoice INV-2026-0143 for Tampa, FL",
"Invoice [INV-2026-0144 for Boise, ID",
"Invoice [] for Reno, NV",
];
for (const text of samples) {
console.log(JSON.stringify(getBetween(text, "[", "]")));
}
const plain = samples[1];
console.log("naive one-liner:", JSON.stringify(plain.slice(plain.indexOf("[") + 1, plain.indexOf("]"))));
console.log("with a fallback:", getBetween(plain, "[", "]") ?? "UNKNOWN");
Output:
"INV-2026-0142"
undefined
undefined
""
naive one-liner: "Invoice INV-2026-0143 for Tampa, F"
with a fallback: UNKNOWN

Look at the naive line. With no brackets, indexOf("[") + 1 becomes 0 and indexOf("]") becomes -1, so slice(0, -1) returns the whole string minus its last letter. No error, just wrong data.
Because the return type is string | undefined, strict mode makes callers handle the miss. Use ?? for a default or an if check before calling string methods.
An empty string is a valid result ([]), and it’s different from undefined. For a yes/no test before extracting, see checking if a string contains a substring in TypeScript.
Get a substring between multi-character delimiters
Tags like <b>, template tokens like {{ and markers like BEGIN: are more than one character long. Adding 1 to the start index then leaves part of the delimiter behind:
function getBetween(text: string, start: string, end: string): string | undefined {
const from = text.indexOf(start);
if (from === -1) return undefined; // start delimiter missing
const begin = from + start.length; // works for "[" and "{{" alike
const to = text.indexOf(end, begin); // look for end AFTER start
if (to === -1) return undefined; // end delimiter missing
return text.slice(begin, to);
}
const html: string = "<b>Chicago</b>, IL";
const at = html.indexOf("<b>");
console.log("+1 bug: ", html.slice(at + 1, html.indexOf("</b>")));
console.log("+tag.length:", html.slice(at + "<b>".length, html.indexOf("</b>")));
console.log(getBetween("Hi {{firstName}}, your total is {{total}}", "{{", "}}"));
console.log(getBetween("2026-10-22 09:14 BEGIN:order 1042 approved:END", "BEGIN:", ":END"));
Output:
+1 bug: b>Chicago
+tag.length: Chicago
firstName
order 1042 approved
The helper avoids this because it adds start.length, not 1. It only finds the first pair, though. Those {{ }} tokens often live in template strings, which I cover in creating a multiline string in TypeScript.
Extract text between brackets with a regex
match() with a capture group does the search and slice in one step. In /\[(.*?)\]/, the brackets are escaped, and the parentheses capture what’s between them. Use the result unchecked and strict TypeScript stops you:
const ticket: string = "Ticket [HD-5531] opened in Phoenix, AZ";
const m = ticket.match(/\[(.*?)\]/);
console.log(m[1].toLowerCase());
Output:
between_strict.ts(3,13): error TS18047: 'm' is possibly 'null'.

match() returns RegExpMatchArray | null, so m[1] won’t compile until you handle null.Optional chaining, ?.[1], fixes it and gives undefined when there’s no match. Here’s how lazy, greedy and character-class patterns behave:
const line: string = "Invoice [INV-2026-0142] for [Austin, TX]";
console.log("lazy: ", line.match(/\[(.*?)\]/)?.[1]);
console.log("greedy: ", line.match(/\[(.*)\]/)?.[1]);
console.log("class: ", line.match(/\[([^\]]*)\]/)?.[1]);
console.log("missing:", "no brackets here".match(/\[(.*?)\]/)?.[1]);
const label: string = "Ship to [Suite 400\nDallas, TX]";
console.log("no s flag:", label.match(/\[(.*?)\]/)?.[1]);
console.log("s flag: ", JSON.stringify(label.match(/\[(.*?)\]/s)?.[1]));
Output:
lazy: INV-2026-0142
greedy: INV-2026-0142] for [Austin, TX
class: INV-2026-0142
missing: undefined
no s flag: undefined
s flag: "Suite 400\nDallas, TX"
The ? in .*? makes the match lazy, so it stops at the first ]. Greedy .* runs to the last one. [^\]]* means “anything except a closing bracket” and is the most explicit option.
Also note that . doesn’t match a line break unless you add the s flag. For splitting text on patterns rather than extracting, see splitting a string by regex in TypeScript.
Get all strings between two characters with matchAll()
match() only returns the first capture. To get every value between brackets, use matchAll() with the g flag; each result carries the capture and its index:
const order: string = "Order [1042] shipped to [Denver, CO] for [$318.40] on [2026-10-22]";
const values = Array.from(order.matchAll(/\[(.*?)\]/g), (m) => m[1]);
console.log(values);
for (const m of order.matchAll(/\[(.*?)\]/g)) {
console.log(`index ${m.index}: ${m[1]}`);
}
try {
const noG = /\[(.*?)\]/;
console.log(Array.from(order.matchAll(noG)));
} catch (err) {
console.log((err as Error).name + ":", (err as Error).message);
}
Output:
[ '1042', 'Denver, CO', '$318.40', '2026-10-22' ]
index 6: 1042
index 24: Denver, CO
index 41: $318.40
index 54: 2026-10-22
TypeError: String.prototype.matchAll called with a non-global RegExp argument

matchAll() throws when the g flag is missing.That error only shows up at runtime; the compiler accepts any RegExp. Array.from() turns the iterator into a plain string[], and a value like $318.40 still needs converting the string to a number before you do math.
When the values sit between different separators instead of matched pairs, splitting a string with multiple separators may be simpler.
Build the regex from variables safely
If the delimiters come from a variable, escape them before passing them to new RegExp(). A dot, a dollar sign or a parenthesis has a special meaning inside a pattern:
function escapeRegExp(text: string): string {
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function betweenAll(text: string, start: string, end: string): string[] {
const re = new RegExp(`${escapeRegExp(start)}(.*?)${escapeRegExp(end)}`, "g");
return Array.from(text.matchAll(re), (m) => m[1]);
}
const version: string = "v2.14.7";
console.log("unescaped:", JSON.stringify(version.match(new RegExp("." + "(.*?)" + "."))?.[1]));
console.log("escaped: ", betweenAll(version, ".", "."));
console.log(betweenAll("Total $84.99 (tax $6.93) (shipping $5.00)", "(", ")"));
console.log(betweenAll("Hello {{first}} {{last}} from {{city}}", "{{", "}}"));
Output:
unescaped: ""
escaped: [ '14' ]
[ 'tax $6.93', 'shipping $5.00' ]
[ 'first', 'last', 'city' ]
Unescaped, each . means “any character”, so the pattern matches v2 and captures nothing. Escaped, it finds 14. For version strings, splitting a string by dot in TypeScript is another option.
Node.js 24 and newer browsers also ship a built-in RegExp.escape(), but it isn’t in Node.js 22, so the small helper above is the portable choice.