skip to content

Primitive & Literal Types

How you name the simplest values in the type system — the primitives, the single-value literal types built on top of them, and the const assertion that keeps literals from widening. This is where most TypeScript conversations start, and where interviewers check whether you understand inference rather than just typing annotations everywhere.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

explore

questions

13

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?

level: juniorimportance: must knowfreq 72%

answer

  1. one is the primitive, one is the box
  2. assignability runs one way only
  3. String is not assignable to string
  4. arithmetic wants number, not Number
  5. lowercase in every annotation

basics

~20 s

Lowercase 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 lines
typescript
const 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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.

context

open as a page

In TypeScript, what type is inferred for `const roles = ['admin', 'user']`, and how does adding `as const` to that array literal change it?

level: juniorimportance: must knowfreq 72%

basics

~10 s

TypeScript infers string[] there: a mutable array of widened strings. Adding as const infers readonly ["admin", "user"] instead — a readonly tuple whose elements keep their exact literal types, with fixed length and position.

open as a page

In TypeScript, what is a string literal type such as 'GET', and what does the union type 'GET' | 'POST' | 'DELETE' allow as a value?

level: juniorimportance: must knowfreq 80%

basics

~20 s

A literal type's domain is a single value: the type 'GET' accepts only the string 'GET'. A union such as 'GET' | 'POST' | 'DELETE' accepts exactly those three strings and rejects every other string.

open as a page

In TypeScript, what type does the compiler infer for `const n = 42` compared with `let n = 42`, and why do the two differ?

level: middleimportance: must knowfreq 66%

basics

~20 s

TypeScript infers the narrow type 42 for const n = 42 and the wider type number for let n = 42. A const can never be reassigned, so the narrow type stays accurate; a let can be, so the compiler widens it to the primitive.

open as a page

In TypeScript, given `const STATUS = { idle: 'idle', busy: 'busy' };`, how do you derive a union type of its values, and what has to be true of the object for that to work?

level: middleimportance: must knowfreq 60%

basics

~20 s

Assert the object with as const so its values keep their literal types, then take an indexed access over its keys: type Status = (typeof STATUS)[keyof typeof STATUS], which is "idle" | "busy". Without as const the values widen and the union collapses to string.

open as a page

This TypeScript code fails to compile: `declare function request(url: string, method: 'GET' | 'POST'): void; const config = { method: 'GET' }; request('/users', config.method);`. What type did the compiler infer for config.method, why, and how would you fix it?

level: middleimportance: must knowfreq 68%

basics

~20 s

The compiler inferred string for config.method, because an object literal's property is a mutable location and its fresh literal type widens. Fix it by annotating the object with the union, adding as const, or passing 'GET' inline.

open as a page

In TypeScript, what values are assignable to `object`, to `{}` and to `Object`, and which of the three actually means "any non-primitive value"?

level: middleimportance: should knowfreq 45%

basics

~20 s

Only lowercase object means "non-primitive": it accepts arrays, functions and class instances and rejects every primitive. The type {} means "anything except null and undefined", so strings and numbers satisfy it, and Object behaves essentially the same way.

open as a page

In TypeScript, what is the difference between writing `as const` on an object literal and writing `satisfies SomeType` after it, and when would you use both together?

level: middleimportance: should knowfreq 47%

basics

~20 s

They do different jobs. as const pins inference: literal types are kept and everything becomes readonly. satisfies checks the literal against a type without replacing the inferred type. Written together, as const satisfies T both pins the values and validates the shape.

open as a page

In TypeScript, what does a parameter annotated `flag: true` accept, and how does the type boolean relate to the literal types true and false?

level: middleimportance: should knowfreq 45%

basics

~20 s

A parameter typed true accepts only the value true, not any truthy value. The type boolean behaves as the union true | false, so true is assignable to boolean but a value typed boolean is not assignable to true.

open as a page

In a TypeScript review you see every local variable annotated, as in `const count: number = items.length`. Is that worth flagging, and where does an explicit annotation on a variable genuinely earn its place?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Mostly yes: annotating what inference already knows adds noise and can silently launder an any, because any is assignable to every type. Annotations earn their place on declarations inference cannot resolve — empty collections, uninitialised or nullish-initialised variables, and exported values.

open as a page

A shared configuration object in a TypeScript service is declared with `as const`. A teammate says this makes it immutable. Is that accurate, and how would you actually guarantee the object is not mutated at runtime?

level: seniorimportance: should knowfreq 40%

basics

~20 s

No. A const assertion is a compile-time constraint that is erased at emit — it produces no runtime protection. Direct writes are rejected by the checker, but any alias typed as mutable, any cast, or any untyped consumer can still change the object. Runtime immutability needs a runtime mechanism.

open as a page

In TypeScript, what is the difference between the `symbol` type and a `unique symbol` type, and where are you allowed to use `unique symbol`?

level: middleimportance: nice to knowfreq 22%

basics

~20 s

The symbol type covers all symbols interchangeably; a unique symbol is a subtype tied to one specific declaration, so the checker can tell it apart from every other symbol. It is only allowed on const declarations and readonly static properties.

open as a page

In TypeScript 5.x, why does the type `'red' | 'blue' | string` behave exactly like `string`, and what is the `(string & {})` idiom that library authors write to avoid it?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

A union absorbs members that are subtypes of another member, and every string literal type is a subtype of string, so the literals are reduced away and only string remains. Writing (string & {}) instead of string blocks that reduction and keeps editor suggestions.

open as a page