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), вызывает метод, а затем выбрасывает этот