📘 TypeScript

التعبيرات المنتظمة في TypeScript: دليل شامل

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты — библиотекарь в огромной библиотеке, где хранятся миллионы книг. К тебе приходит читатель и говорит: «Мне нужны все книги, в названии которых есть слово "приключения", но только те, что написаны после 2000 года, и чтобы на обложке была картинка с драконом». Если бы ты просматривал каждую книгу вручную, это заняло бы недели. Но у тебя есть волшебная лупа, которая умеет мгновенно находить книги по описанию. Ты просто описываешь лупе, что искать, и она за секунду показывает все подходящие книги.

Регулярные выражения (Regular Expressions, или сокращённо regexp, regex) — это и есть такая «волшебная лупа» для текста. Это специальный язык, который позволяет описывать шаблоны (паттерны) поиска в строках. С помощью регулярных выражений ты можешь:

  • Проверить, соответствует ли строка определённому формату (например, является ли строка email-адресом или номером телефона);
  • Найти в тексте все слова, которые подходят под описание (например, все слова из 5 букв, начинающиеся на «а»);
  • Заменить часть текста по шаблону (например, заменить все даты из формата «дд.мм.гггг» на «гггг-мм-дд»);
  • Разбить строку на части по определённому правилу.
💡 Важно понимать: Регулярные выражения — это не функция и не метод конкретного языка программирования. Это самостоятельный язык, который встроен в TypeScript (и JavaScript) и во многие другие языки. Изучив его один раз, ты сможешь использовать его везде!

В реальной жизни регулярные выражения используются повсеместно: от проверки форм на сайтах (правильно ли введён пароль?) до поиска ошибок в коде и обработки огромных массивов данных. Без них работа программиста была бы в разы сложнее и медленнее.

Теория: как это работает изнутри

Чтобы понять, как работают регулярные выражения, нужно разобраться с их «кирпичиками». Любое регулярное выражение — это последовательность символов, которая описывает шаблон. Движок регулярных выражений (встроенная программа в TypeScript) берёт этот шаблон и «проходит» по строке, пытаясь найти совпадение.

Основные понятия

1. Литералы (обычные символы)
Самый простой случай: если ты напишешь в шаблоне обычный текст, движок будет искать этот текст буквально. Например, шаблон cat найдёт в строке «I have a cat» слово «cat». Но он не найдёт «Cat» (с большой буквы), потому что регистр имеет значение.

2. Метасимволы (специальные символы)
Это символы, которые имеют особое значение. Вот самые важные из них:

Символ Значение Пример
. Любой одиночный символ (кроме перевода строки) c.t найдёт «cat», «cot», «c4t»
d Любая цифра (0-9) dd найдёт «42», «00», «99»
w Любая буква, цифра или знак подчёркивания ww найдёт «a1», «_b», «X9»
s Любой пробельный символ (пробел, табуляция, перевод строки) wordsword найдёт «word word»
^ Начало строки ^Hello найдёт «Hello» только в начале строки
$ Конец строки world$ найдёт «world» только в конце строки
 Граница слова cat найдёт «cat», но не «catalog»

3. Квантификаторы (указатели количества)
Они говорят, сколько раз должен повториться предыдущий символ или группа:

Квантификатор Значение Пример
* 0 или более раз go* найдёт «g», «go», «goo», «gooo»
+ 1 или более раз go+ найдёт «go», «goo», но не «g»
? 0 или 1 раз (необязательный символ) colou?r найдёт «color» и «colour»
{n} Ровно n раз d{3} найдёт ровно 3 цифры подряд
{n,} n или более раз d{2,} найдёт 2 и более цифр
{n,m} От n до m раз d{2,4} найдёт от 2 до 4 цифр

4. Классы символов (группы)
Иногда нужно указать не один символ, а целый набор возможных вариантов. Для этого используются квадратные скобки:

  • [abc] — любой из символов a, b или c;
  • [a-z] — любая строчная буква латинского алфавита;
  • [0-9] — любая цифра (то же самое, что d);
  • [^abc] — любой символ, кроме a, b или c (знак ^ внутри скобок означает отрицание).

5. Группы и альтернативы
Круглые скобки () используются для группировки частей шаблона. Это нужно, чтобы применить квантификатор к целой группе или чтобы «запомнить» найденную часть для дальнейшего использования. Вертикальная черта | означает «или»:

  • (cat|dog) найдёт либо «cat», либо «dog»;
  • (ha){3} найдёт «hahaha» (три повторения группы «ha»).
⚠️ Важное замечание: В TypeScript (и JavaScript) регулярные выражения создаются двумя способами: с помощью литерала /pattern/flags или конструктора new RegExp('pattern', 'flags'). Литерал удобнее для статических шаблонов, конструктор — когда шаблон создаётся динамически.

Синтаксис в TypeScript: все формы и варианты

В TypeScript для работы с регулярными выражениями используется встроенный объект RegExp. Давай рассмотрим все способы его создания и основные методы.

Создание регулярного выражения

// Способ 1: Литерал (самый распространённый)
// Шаблон заключается в слеши, после них могут идти флаги
const regexLiteral: RegExp = /hello/gi;

// Способ 2: Конструктор (полезен, когда шаблон динамический)
const pattern: string = "hello";
const flags: string = "gi";
const regexConstructor: RegExp = new RegExp(pattern, flags);

// Оба способа создают объект одного и того же типа RegExp
console.log(typeof regexLiteral); // "object"
console.log(regexLiteral instanceof RegExp); // true

Флаги (модификаторы поведения)

Флаги — это буквы, которые идут после закрывающего слеша (или вторым аргументом конструктора) и меняют поведение поиска:

Флаг Название Описание
g global Ищет все совпадения, а не только первое
i ignore case Игнорирует регистр букв
m multiline Меняет поведение ^ и $ — они начинают работать с каждой строкой внутри текста
s dotAll Позволяет точке . находить также символ перевода строки
u unicode Включает полную поддержку Unicode (нужен для работы с эмодзи и спецсимволами)
y sticky Ищет совпадение только начиная с текущей позиции в строке

Основные методы RegExp и String

Регулярные выражения в TypeScript используются совместно с методами строк и самого объекта RegExp. Вот самые важные из них:

Метод Где определён Что делает Возвращает
test() RegExp Проверяет, есть ли совпадение в строке boolean
exec() RegExp Ищет совпадение и возвращает подробную информацию RegExpExecArray | null
match() String Ищет совпадения в строке string[] | null
matchAll() String Ищет все совпадения (с флагом g) и возвращает итератор IterableIterator<RegExpExecArray>
replace() String Заменяет совпадения на указанную строку string
replaceAll() String Заменяет все совпадения (не требует флага g) string
search() String Ищет первое совпадение и возвращает его индекс number (-1 если не найдено)
split() String Разбивает строку по совпадениям с шаблоном string[]

Специальные символы в строках (экранирование)

Если тебе нужно найти символ, который является метасимволом (например, точку или звёздочку), его нужно «экранировать» — поставить перед ним обратную косую черту :

// Ищем буквально точку в конце предложения
const regexDot: RegExp = /.$/;
console.log(regexDot.test("Привет.")); // true
console.log(regexDot.test("Привет"));  // false

// Ищем звёздочку в тексте
const regexStar: RegExp = /*/;
console.log(regexStar.test("2 * 3 = 6")); // true
💡 Совет: В TypeScript, если ты используешь конструктор new RegExp(), помни, что обратную косую черту нужно дублировать, потому что строки тоже используют её для экранирования. Например: new RegExp('\.') будет искать точку.

Примеры кода — от простого к сложному

Теперь давай посмотрим на реальные примеры. Каждый пример — полностью рабочий код с подробными комментариями.

Пример 1: Проверка, содержит ли строка цифры

// Задача: проверить, есть ли в строке хотя бы одна цифра
const text1: string = "Привет, мне 25 лет!";
const text2: string = "Привет, мир!";

// Создаём регулярное выражение: d — любая цифра
const hasDigitRegex: RegExp = /d/;

// Метод test() возвращает true или false
console.log(`"${text1}" содержит цифры?`, hasDigitRegex.test(text1)); // true
console.log(`"${text2}" содержит цифры?`, hasDigitRegex.test(text2)); // false

// Объяснение: d — это класс символов, который соответствует любой цифре от 0 до 9.
// Метод test() просто проверяет, есть ли хотя бы одно совпадение в строке.

Пример 2: Поиск всех email-адресов в тексте

// Задача: найти все email-адреса в тексте
const emailText: string = "Свяжитесь с нами: support@example.com или info@test.org. Также пишите на admin@site.net.";

// Шаблон для email (упрощённый):
// [w.-]+ — один или более символов: буквы, цифры, _, ., -
// @ — символ собаки
// [w.-]+ — домен
// . — точка (экранированная)
// [a-zA-Z]{2,} — доменная зона (от 2 букв)
const emailRegex: RegExp = /[w.-]+@[w.-]+.w{2,}/g;

// Метод match() возвращает массив всех совпадений (благодаря флагу g)
const foundEmails: string[] | null = emailText.match(emailRegex);

if (foundEmails) {
    console.log("Найденные email-адреса:", foundEmails);
    // Вывод: ["support@example.com", "info@test.org", "admin@site.net"]
} else {
    console.log("Email-адреса не найдены");
}

// Объяснение: Флаг g (global) заставляет искать все совпадения, а не только первое.
// Класс [w.-] включает буквы, цифры, подчёркивание, точку и дефис.
// .{2,} означает "от 2 символов" для доменной зоны (com, org, net и т.д.)

Пример 3: Проверка номера телефона в формате +7 (XXX) XXX-XX-XX

// Задача: проверить, соответствует ли строка формату российского номера телефона
const phone1: string = "+7 (123) 456-78-90";
const phone2: string = "8 (123) 456-78-90";
const phone3: string = "+7 123 456 78 90";

// Шаблон:
// ^ — начало строки
// +7 — символ + и цифра 7 (плюс экранируем, так как + — метасимвол)
// s — пробел
// ( — открывающая скобка (экранируем)
// d{3} — ровно 3 цифры
// ) — закрывающая скобка
// s — пробел
// d{3} — 3 цифры
// - — дефис
// d{2} — 2 цифры
// - — дефис
// d{2} — 2 цифры
// $ — конец строки
const phoneRegex: RegExp = /^+7s(d{3})sd{3}-d{2}-d{2}$/;

console.log(`"${phone1}" — корректный номер?`, phoneRegex.test(phone1)); // true
console.log(`"${phone2}" — корректный номер?`, phoneRegex.test(phone2)); // false (нет +7)
console.log(`"${phone3}" — корректный номер?`, phoneRegex.test(phone3)); // false (нет скобок и дефисов)

// Объяснение: ^ и $ — якоря, которые требуют, чтобы строка полностью соответствовала шаблону.
// Без них шаблон искал бы совпадение в любой части строки.
// Экранирование ( и ) нужно, чтобы искать буквальные скобки, а не группу.

Пример 4: Замена дат из формата ДД.ММ.ГГГГ на ГГГГ-ММ-ДД

// Задача: преобразовать даты в международный формат
const dateText: string = "Сегодня 15.03.2024, а завтра будет 16.03.2024.";

// Шаблон для поиска даты:
// (d{2}) — группа 1: две цифры (день)
// . — точка
// (d{2}) — группа 2: две цифры (месяц)
// . — точка
// (d{4}) — группа 3: четыре цифры (год)
const dateRegex: RegExp = /(d{2}).(d{2}).(d{4})/g;

// В строке замены $1, $2, $3 ссылаются на захваченные группы
// Меняем порядок: год-месяц-день
const convertedText: string = dateText.replace(dateRegex, "$3-$2-$1");

console.log("Исходный текст:", dateText);
console.log("После замены:", convertedText);
// Вывод: "Сегодня 2024-03-15, а завтра будет 2024-03-16."

// Объяснение: Круглые скобки создают "захватывающие группы".
// В строке замены $1 означает "первая захваченная группа", $2 — вторая и т.д.
// Это мощный инструмент для перестановки частей текста.

Пример 5: Извлечение всех URL из HTML-текста

// Задача: найти все ссылки в HTML-коде
const htmlText: string = `
    Сайт 1
    Сайт 2
    Сайт 3
`;

// Шаблон для поиска href:
// href= — буквально
// ["'] — открывающая кавычка (одинарная или двойная)
// ([^"']+) — группа 1: один или более символов, не являющихся кавычкой
// ["'] — закрывающая кавычка
const urlRegex: RegExp = /href=["']([^"']+)["']/g;

// Используем matchAll() для получения подробной информации о каждом совпадении
const matches = htmlText.matchAll(urlRegex);

console.log("Найденные URL:");
for (const match of matches) {
    // match[0] — полное совпадение (например, href="https://example.com")
    // match[1] — первая захваченная группа (сам URL)
    console.log(`  - ${match[1]}`);
}
// Вывод:
//   - https://example.com
//   - http://test.org/page
//   - https://site.net?query=value

// Объяснение: [^"']+ означает "один или более символов, которые НЕ являются кавычкой".
// Это гарантирует, что мы захватим всё содержимое между кавычками.
// matchAll() возвращает итератор, который можно обойти в цикле for...of.

Пример 6: Проверка сложности пароля

// Задача: проверить, что пароль соответствует требованиям:
// - минимум 8 символов
// - хотя бы одна заглавная буква
// - хотя бы одна строчная буква
// - хотя бы одна цифра
// - хотя бы один специальный символ

function validatePassword(password: string): { isValid: boolean; errors: string[] } {
    const errors: string[] = [];

    // Проверка длины: ^.{8,}$ — строка из 8 и более любых символов
    if (!/^.{8,}$/.test(password)) {
        errors.push("Пароль должен содержать минимум 8 символов");
    }

    // Проверка заглавной буквы: [A-Z] — любая заглавная латинская буква
    if (!/[A-Z]/.test(password)) {
        errors.push("Пароль должен содержать хотя бы одну заглавную букву");
    }

    // Проверка строчной буквы: [a-z] — любая строчная латинская буква
    if (!/[a-z]/.test(password)) {
        errors.push("Пароль должен содержать хотя бы одну строчную букву");
    }

    // Проверка цифры: d — любая цифра
    if (!/d/.test(password)) {
        errors.push("Пароль должен содержать хотя бы одну цифру");
    }

    // Проверка спецсимвола: [^A-Za-z0-9] — любой символ, не являющийся буквой или цифрой
    if (!/[^A-Za-z0-9]/.test(password)) {
        errors.push("Пароль должен содержать хотя бы один специальный символ");
    }

    return {
        isValid: errors.length === 0,
        errors: errors
    };
}

// Тестируем
const testPasswords: string[] = ["weak", "Strong1", "Strong1!"];

testPasswords.forEach(pwd => {
    const result = validatePassword(pwd);
    console.log(`Пароль "${pwd}": ${result.isValid ? "✓ Корректен" : "✗ Ошибки: " + result.errors.join(", ")}`);
});
// Вывод:
// Пароль "weak": ✗ Ошибки: Пароль должен содержать минимум 8 символов, ...
// Пароль "Strong1": ✗ Ошибки: Пароль должен содержать хотя бы один специальный символ
// Пароль "Strong1!": ✓ Корректен

// Объяснение: Вместо одного сложного регулярного выражения мы используем несколько простых.
// Это делает код понятнее и позволяет давать конкретные сообщения об ошибках.

Пример 7: Разбор логов сервера

// Задача: извлечь информацию из строки лога Apache/Nginx
const logLine: string = '192.168.1.1 - - [10/Mar/2024:13:55:36 +0000] "GET /index.html HTTP/1.1" 200 2326';

// Шаблон для разбора лога:
// (S+) — группа 1: IP-адрес (один или более непробельных символов)
// s+ — один или более пробелов
// [ — открывающая скобка (экранированная)
// ([^]]+) — группа 2: дата (один или более символов, не являющихся ])
// ] — закрывающая скобка
// s+ — пробелы
// " — кавычка
// ([^"]+) — группа 3: HTTP-запрос (один или более символов, не являющихся ")
// " — кавычка
// s+ — пробелы
// (d+) — группа 4: статус-код (одна или более цифр)
// s+ — пробелы
// (d+) — группа 5: размер ответа (одна или более цифр)
const logRegex: RegExp = /(S+)s+S+s+S+s+[([^]]+)]s+"([^"]+)"s+(d+)s+(d+)/;

const logMatch = logLine.match(logRegex);

if (logMatch) {
    console.log("Разбор лога:");
    console.log(`  IP-адрес: ${logMatch[1]}`);        // 192.168.1.1
    console.log(`  Дата и время: ${logMatch[2]}`);    // 10/Mar/2024:13:55:36 +0000
    console.log(`  HTTP-запрос: ${logMatch[3]}`);     // GET /index.html HTTP/1.1
    console.log(`  Статус-код: ${logMatch[4]}`);      // 200
    console.log(`  Размер ответа: ${logMatch[5]} байт`); // 2326
} else {
    console.log("Не удалось разобрать лог");
}

// Объяснение: S+ означает "один или более непробельных символов".
// [^]]+ — это класс с отрицанием: "один или более символов, не являющихся ]".
// Этот пример показывает, как с помощью одного регулярного выражения можно разобрать сложную строку.

Сравнение с Python: чем похоже и чем отличается

Если ты уже знаком с Python или планируешь изучать его, полезно знать различия в работе с регулярными выражениями. В Python используется модуль re, а в TypeScript — встроенный объект RegExp.

Характеристика TypeScript (JavaScript) Python
Создание Литерал /pattern/flags или new RegExp() Только через re.compile() или функции модуля
Флаги После закрывающего слеша: /pattern/gi Аргументом: re.findall(pattern, text, re.IGNORECASE)
Поиск всех совпадений str.match(regex) с флагом g re.findall(pattern, text) (всегда ищет все)
Замена str.replace(regex, replacement) re.sub(pattern, replacement, text)
Именованные группы (?<name>pattern) (?P<name>pattern) (другой синтаксис)
Unicode Флаг u для полной поддержки По умолчанию поддерживает Unicode (строки — Unicode)
Режим многострочности Флаг m меняет поведение ^ и $ Флаг re.MULTILINE
Производительность Хорошая, но может страдать при сложных шаблонах В целом быстрее для сложных операций

Главные отличия:

  • Синтаксис создания: В TypeScript ты можешь использовать удобный литерал /.../, в Python — только вызов функций.
  • Флаг global: В TypeScript без флага g методы match() и exec() возвращают только первое совпадение. В Python re.findall() всегда возвращает все.
  • Именованные группы: Синтаксис отличается: в TypeScript (?<name>...), в Python (?P<name>...).
  • Работа с Unicode: В Python строки по умолчанию Unicode, в TypeScript нужно явно указывать флаг u для корректной работы с некоторыми символами.
💡 Совет: Если ты переходишь с Python на TypeScript (или наоборот), обращай внимание на флаги. Самая частая ошибка — забыть флаг g в TypeScript, когда нужно найти все совпад