In TypeScript, what is the difference between annotating a value with the lowercase type `string` and the capitalised type `String`, and which one should you write?
answer
- one is the primitive, one is the box
- assignability runs one way only
- String is not assignable to string
- arithmetic wants number, not Number
- lowercase in every annotation
basics
~20 sLowercase string is the primitive type; capitalised String is the standard-library interface describing the boxed wrapper object. A string is assignable to String, but a String is not assignable to string, so always annotate with the lowercase primitives.
solid answer
~40 s`string`, `number`, `boolean`, `bigint` and `symbol` are the primitive types the checker actually reasons about. The capitalised `String`, `Number`, `Boolean`, `BigInt` and `Symbol` are interfaces in the standard library that describe the wrapper objects, and they also supply the members a primitive appears to have. Assignability runs one way only: `string` is assignable to `String` because a string has every member that interface declares, but `String` is not assignable to `string`, so a value annotated `String` cannot be passed where a `string` is expected. Wrapper annotations also lose operator support — the checker rejects `n * 2` when `n: Number`, because an arithmetic operand must be `number`, `bigint`, `any` or an enum type. So annotate with the lowercase names; the capitalised identifiers are for value positions such as calling `String(x)`, not for annotations.
code
typescript · 13 linesconst primitive: string = "hello";
const boxed: String = primitive; // ok: string is assignable to String
// @ts-expect-error String is not assignable to string
const back: string = boxed;
function shout(s: string) {
return s.toUpperCase();
}
shout(primitive);
// @ts-expect-error argument of type 'String' is not assignable to 'string'
shout(boxed);go deeper
Know that the lowercase names — string, number, boolean — are the ones you write in annotations, and that the capitalised ones describe wrapper objects. Saying "always lowercase" plainly is enough at this level.
Explain the mechanics: the capitalised names are standard-library interfaces, the primitive is assignable to the interface but not the reverse, and members resolve through the primitive's apparent type. Mention that arithmetic operands must be number or bigint.
Be ready to trace a real failure — a wrapper type entering from a dependency's declarations or an old codebase — and explain why it only shows up at call sites downstream. Talk about containing it at a boundary rather than asserting at each use.
Own the policy question: this is a defect class you prevent with lint rules and reviewed declaration boundaries rather than case-by-case fixes. Weigh the cost of patching third-party types against wrapping them in an adapter your codebase controls.
## Two names, two different types TypeScript gives you a lowercase name and a capitalised name that look interchangeable and are not. - `string`, `number`, `boolean`, `bigint`, `symbol` are **primitive types**. They are built into the checker and describe primitive values. - `String`, `Number`, `Boolean`, `BigInt`, `Symbol` are **interfaces declared in the standard library** (`lib.es5.d.ts` and friends). Each one describes the *object* form of that primitive — the thing a `new String("a")` call produces at runtime — and declares the members such as `charAt`, `toFixed`, `toString` and `valueOf`. The capitalised identifiers are also *values*: `String`, `Number` and `Symbol` are global functions you can call. That double life is exactly why the mistake is easy to make — the same word is legal in both a value position and a type position, but it means different things there. ## Assignability runs one way TypeScript is structural: `X` is assignable to `Y` when `X` has everything `Y` requires. A primitive string has every member declared on the `String` interface, so: ```ts const s: String = "hello"; // ok ``` The reverse fails. A `String` object is not a primitive, and the checker keeps them apart: ```ts declare const boxed: String; const back: string = boxed; // error: 'String' is not assignable to 'string' ``` So the wrapper annotation is not merely a stylistic slip. It is a **wider** type that quietly stops flowing into the ordinary `string` world: every function in your codebase, and in every library, takes `string`, and your `String`-typed value cannot be passed to any of them. ## Why `"abc".length` still type-checks A primitive has no properties of its own, yet the checker happily resolves `"abc".length` and `(3.14).toFixed(1)`. That is because each primitive type has an **apparent type**: when you look up a member on `string`, the checker consults the `String` interface; on `number`, the `Number` interface. That relationship is exactly why `string` is assignable to `String` — and it means you never need to write the wrapper type to get the methods. You already have them. ## Operators care about the primitive type Member access is the forgiving part. Operators are not: ```ts function twice(n: Number) { return n * 2; // error: an arithmetic operand must be // 'any', 'number', 'bigint' or an enum type } ``` The same applies to comparisons and to anything else that is specified over primitives. A `Number` parameter therefore produces a function that accepts numbers (because `number` is assignable to `Number`) but cannot actually do arithmetic with them — the worst of both directions. ## Nothing here exists at runtime Annotations are erased. Writing `const s: String = "hello"` does **not** box the value; the emitted JavaScript is `const s = "hello"`, and the value stays a primitive. The annotation only changes what the checker will allow, which is precisely why the bug is invisible until some call site refuses your value. Conversely, `String` in a *value* position — `String(x)`, `new String(x)` — is real runtime code that survives compilation. ## `object`, `{}` and `Object` The same lowercase/capital trap exists on the object side, and it behaves differently again: `Object` is an interface that almost everything satisfies, including primitives, so it is not a way to say "this is an object". Treat that as its own question; the rule you carry from here is narrower and absolute — **for primitives, write the lowercase name**. ## What to do about it There is no compiler flag that bans wrapper annotations; the compiler considers them legal types. Codebases usually rely on a lint rule from the typescript-eslint family that flags wrapper-object types in annotations, and on code review. The one legitimate reason to write `String` or `Number` in a type position is when you genuinely mean the boxed object — modelling a value produced by `new String(...)`, which is rare enough that most codebases never need it. ## The short version Lowercase = the primitive the checker reasons about and every API takes. Capitalised = an interface describing the box, assignable *from* the primitive but not *to* it, and not usable with arithmetic. Write the lowercase one, every time.
- If `string` is a primitive with no properties of its own, why does the checker accept `"abc".length`?Each primitive type has an apparent type — the matching standard-library interface. Member lookup on `string` is resolved against `String`, on `number` against `Number`, and so on. That is also the reason the primitive is assignable to the wrapper interface: it structurally has everything the interface declares. You get the members without ever writing the wrapper type yourself.
- A dependency's `.d.ts` types a parameter as `Number`. What breaks for you, and what can you do?Calls still work, because `number` is assignable to `Number`. The pain is on the way out: if a function *returns* `Number`, you cannot use the result in arithmetic or pass it where a `number` is expected. Practical fixes are to wrap the call in your own adapter that returns `number`, or to patch the types locally rather than sprinkling assertions at every use site.
- Is there ever a legitimate reason to write `String` or `Number` in a type position?Only when you genuinely mean the boxed object rather than the primitive — for example describing a value that really came from `new String(...)`. That is rare in modern code. The capitalised names remain entirely normal in *value* positions, where `String(x)` and `Number(x)` are ordinary runtime calls, and that is where you will legitimately see them.
saying these in an interview costs you the question
- Says string and String are interchangeable aliases.
- Thinks a String annotation boxes the value at runtime.
- Assumes assignability works in both directions.
- Believes a Number-typed value supports arithmetic.
- Claims the compiler rejects wrapper annotations outright.