String vs string: the silent killer of your code's performance
\n\nHave you ever wondered why TypeScript complains about String in one place but not about string in another? Or maybe you've seen experienced developers wince at the sight of new String("hello")? This question is a real hit on Stack Overflow, garnering thousands of views. And for good reason. The difference between String and string is not just a quirk of the language, but a fundamental distinction that affects the performance, safety, and readability of your code.
In this article, we won't just give a dry definition. We'll break down what lies behind each of these types, how they behave at runtime, and why making the right choice can speed up your application by tens of percent. Get ready, it's going to be interesting!
\n\nPrimitives vs Objects: the basics
\n\nTo understand the difference, you need to remember how JavaScript (and therefore TypeScript) works with data. The language has two types of values:
\n\n- \n
- Primitives:
string,number,boolean,null,undefined,symbol,bigint. They are immutable and stored by value. \n - Objects:
Object,Array,Function, as well asString(with a capital letter),Number,Boolean. They are mutable and stored by reference. \n
When you write let name: string = "Alice", you create a primitive. When you write let name: String = new String("Alice"), you create a wrapper object. And this is where things get interesting.
The string type (lowercase)
\nThis is a primitive type. It is used in 99.9% of cases. It is lightweight, fast, and predictable. Comparison of two primitives is done by value:
\n\nlet a: string = "hello";\nlet b: string = "hello";\nconsole.log(a === b); // true\n\nTypeScript recommends using this one. It is the de facto standard for declaring string variables, function parameters, and return values.
\n\nThe String type (uppercase)
\nThis is an object type, a wrapper constructor. It has existed since the days of JavaScript for working with string methods. However, its direct use is more of an anti-pattern, unless you have a very specific task. Comparison of two String objects is done by reference:
let a: String = new String("hello");\nlet b: String = new String("hello");\nconsole.log(a === b); // false! These are different objects in memory\n\nThis can lead to hard-to-find bugs. Imagine you are checking if a string value equals "hello", but you get false just because someone used new String().
Performance: why does string win?
\n\nNow let's get to the best part — numbers and optimization. Let's write a simple performance test (you can run it in your environment, for example, in Node.js or in a browser).
\n\n// Performance test\nconst iterations = 1_000_000;\n\nconsole.time("string (primitive)");\nlet sum = "";\nfor (let i = 0; i < iterations; i++) {\n sum += "a";\n}\nconsole.timeEnd("string (primitive)");\n\nconsole.time("String (object)");\nlet sumObj = new String("");\nfor (let i = 0; i < iterations; i++) {\n sumObj += "a"; // Autoboxing and unboxing happens here\n}\nconsole.timeEnd("String (object)");\n\nThe result on a typical machine will be dramatically different (in favor of the primitive by 2-5 times). Why? Because:
\n- \n
- The primitive
stringis simply a sequence of characters in memory. Operations with it are optimized at the engine level.