String vs string в TypeScript: что выбрать для скорости?

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

String vs string: тихий убийца производительности вашего кода

Вы когда-нибудь задумывались, почему в одном месте TypeScript ругается на String, а в другом — на string? Или, может быть, вы видели, как опытные разработчики морщатся при виде new String("hello")? Этот вопрос — настоящий хит на Stack Overflow, набравший тысячи просмотров. И неспроста. Разница между String и string — это не просто причуда языка, а фундаментальное различие, которое влияет на производительность, безопасность и читаемость вашего кода.

В этой статье мы не просто дадим сухое определение. Мы разберем, что стоит за каждым из этих типов, как они ведут себя в рантайме и почему правильный выбор может ускорить ваше приложение на десятки процентов. Готовьтесь, будет интересно!

Примитивы vs Объекты: основы основ

Чтобы понять разницу, нужно вспомнить, как JavaScript (а значит, и TypeScript) работает с данными. В языке есть два типа значений:

  • Примитивы (primitives): string, number, boolean, null, undefined, symbol, bigint. Они неизменяемы и хранятся по значению.
  • Объекты (objects): Object, Array, Function, а также String (с большой буквы), Number, Boolean. Они изменяемы и хранятся по ссылке.

Когда вы пишете let name: string = "Alice", вы создаете примитив. Когда вы пишете let name: String = new String("Alice"), вы создаете объект-обертку. И вот тут начинается самое интересное.

Тип string (с маленькой буквы)

Это примитивный тип. Он используется в 99.9% случаев. Он легковесный, быстрый и предсказуемый. Сравнение двух примитивов происходит по значению:

let a: string = "hello";
let b: string = "hello";
console.log(a === b); // true

TypeScript рекомендует использовать именно его. Это стандарт де-факто для объявления строковых переменных, параметров функций и возвращаемых значений.

Тип String (с большой буквы)

Это объектный тип, конструктор-обертка. Он существует со времен JavaScript для работы с методами строк. Однако, его прямое использование — это скорее антипаттерн, если только у вас нет очень специфической задачи. Сравнение двух объектов String происходит по ссылке:

let a: String = new String("hello");
let b: String = new String("hello");
console.log(a === b); // false! Это разные объекты в памяти

Это может привести к трудноуловимым багам. Представьте, что вы проверяете, равно ли значение строки "hello", а получаете false только потому, что кто-то использовал new String().

Производительность: почему string побеждает?

Теперь перейдем к самому вкусному — к цифрам и оптимизации. Давайте напишем простой тест производительности (вы можете запустить его в своей среде, например, в Node.js или в браузере).

// Тест производительности
const iterations = 1_000_000;

console.time("string (primitive)");
let sum = "";
for (let i = 0; i < iterations; i++) {
    sum += "a";
}
console.timeEnd("string (primitive)");

console.time("String (object)");
let sumObj = new String("");
for (let i = 0; i < iterations; i++) {
    sumObj += "a"; // Здесь происходит автоупаковка и распаковка
}
console.timeEnd("String (object)");

Результат на типичной машине будет разительно отличаться (в пользу примитива в 2-5 раз). Почему? Потому что:

  • Примитив string — это просто последовательность символов в памяти. Операции с ним оптимизированы на уровне движка V8 (или SpiderMonkey).
  • Объект String — это полноценный объект с кучей методов, прототипом и накладными расходами на создание и уничтожение. При каждой операции конкатенации (+=) движок вынужден создавать новый объект, что приводит к утечкам памяти и замедлению сборки мусора.

Более того, TypeScript сам по себе — это инструмент статического анализа. Он работает во время компиляции. Но если вы используете String в своих объявлениях, вы лишаете себя возможности использовать некоторые продвинутые возможности вывода типов и сужения (narrowing).

Практические примеры: когда что использовать?

1. Объявление переменных и параметров

Правильно:

function greet(name: string): string {
    return `Hello, ${name}!`;
}

Неправильно (но технически верно):

function greet(name: String): String {
    return `Hello, ${name}!`;
}

Почему неправильно? Потому что это вводит в заблуждение. Другой разработчик подумает, что вы ожидаете объект, а не примитив, и может передать new String("..."), что приведет к описанным выше проблемам.

2. Работа с API и библиотеками

Многие библиотеки (например, для работы с DOM, HTTP-запросами) ожидают примитивные строки. Если вы передадите им объект String, они все равно преобразуют его в примитив (через вызов .toString()), но это лишняя операция.

// Плохо
const url: String = new String("https://api.example.com");
fetch(url); // Работает, но с преобразованием

// Хорошо
const url: string = "https://api.example.com";
fetch(url); // Быстро и эффективно

3. Когда String может быть полезен?

Честно говоря, в современном TypeScript-разработке крайне редко. Единственный сценарий — если вам нужно расширить прототип строки (что само по себе плохая практика) или если вы работаете с очень старым кодом, который использует instanceof для проверки типа:

if (value instanceof String) {
    // Это может сработать только для объектов String
}

Но даже в этом случае лучше использовать typeof value === 'string'.

TypeScript и автобоксинг: почему вы не замечаете разницы?

"Но постойте, — скажете вы, — я же использую string, но могу вызывать методы типа .toUpperCase() или .trim(). Как так?"

Это магия автобоксинга (autoboxing). Когда вы вызываете метод на примитиве, JavaScript временно оборачивает его в объект-обертку (String), вызывает метод, а затем выбрасывает этот

Блоги

Рекомендаций по книгам