skip to content

Which four string-manipulation types does the TypeScript compiler provide as built-ins, what does each do, and why can you not implement them yourself in the type system?

level: middleimportance: nice to knowfreq 38%

answer

  1. four of them, no more
  2. two change every character, two change one
  3. declared as intrinsic in the lib file
  4. no character-level operation in the type system
  5. erased — no toUpperCase is emitted

basics

~10 s

Uppercase, Lowercase, Capitalize and Uncapitalize. Each transforms a string literal type, and they are declared as intrinsic in lib.es5.d.ts, meaning the compiler implements them natively because the type system has no character-level string operation.

solid answer

~40 s

They are `Uppercase`, `Lowercase`, `Capitalize` and `Uncapitalize`. `Uppercase<"abc">` is `"ABC"`, `Lowercase<"ABC">` is `"abc"`, `Capitalize<"hello world">` is `"Hello world"` — first character only — and `Uncapitalize` lowers just the first character. They distribute over unions, so `Uppercase<"a" | "b">` is `"A" | "B"`, and applying one to `string` gives back `string`. You cannot write them yourself because the type layer offers no way to inspect or transform an individual character: `infer` can split a string literal into pieces, but nothing maps `"a"` to `"A"`. That is why `lib.es5.d.ts` declares them as `type Uppercase<S extends string> = intrinsic`, with `intrinsic` a marker only the compiler's own library files may use — writing it in your own code is an error. In practice they pair with template literal types to derive names like `` `on${Capitalize<E>}` ``.

code

typescript · 14 lines
typescript
type Event = "click" | "focus" | "keydown";

// First character only, and it distributes over the union
type HandlerName = `on${Capitalize<Event>}`;
// "onClick" | "onFocus" | "onKeydown"

const handlers: Record<HandlerName, () => void> = {
  onClick: () => {},
  onFocus: () => {},
  onKeydown: () => {},
};

type Loud = Uppercase<"id" | "name">; // "ID" | "NAME"
type Same = Uppercase<string>;        // string

go deeper

for a junior

Recall the four names and what each does, and be clear that Capitalize changes only the first character. Say plainly that they operate on types, not on runtime strings.

for a middle

Explain distribution over unions, the identity behaviour on string, and why the type system cannot express character transformation on its own — hence the intrinsic declaration in the lib file.

for a senior

Show where they pay off in real code: deriving a handler-name or environment-variable vocabulary from one source union so the two conventions cannot drift, and recognising when a derived type has silently collapsed to string.

for a principal

Judge whether deriving naming conventions in the type layer is worth the indirection for the team. Weigh the guaranteed consistency against errors that print derived types nobody wrote and are harder for newcomers to trace.

## The four types TypeScript ships exactly four string-manipulation types, and they operate on string literal types rather than on values: ```ts type A = Uppercase<"hello">; // "HELLO" type B = Lowercase<"HELLO">; // "hello" type C = Capitalize<"hello world">; // "Hello world" type D = Uncapitalize<"HelloWorld">; // "helloWorld" ``` The pair that trips people up is `Capitalize` and `Uncapitalize`: they touch the **first character only**. `Capitalize<"hello world">` is `"Hello world"`, not `"Hello World"` — there is no word-splitting anywhere in the type system. ## How they behave on non-single-literal inputs They distribute over unions, one result per member: ```ts type Keys = Uppercase<"id" | "name">; // "ID" | "NAME" ``` Applied to the primitive, they are the identity: `Uppercase<string>` is `string`, because the compiler cannot know which characters are present, and every uppercase string is still a string. Applied to a pattern type, the fixed segments transform and the hole stays open. None of this is arithmetic on values — the input must be a type, so `Uppercase<typeof someVariable>` works only when that variable has a literal type (typically because it was declared `const` or asserted `as const`). ## Why you cannot write them Type-level string work in TypeScript is built from two primitives: template literal types, which construct and pattern-match strings, and `infer`, which captures a matched piece. You can split a literal into its first character and the rest: ```ts type Head<S extends string> = S extends `${infer H}${string}` ? H : never; type H = Head<"abc">; // "a" ``` What you cannot do is transform that character. There is no type-level table mapping `"a"` to `"A"`, no character-code operation, no comparison operator over characters. The only way to write case conversion in pure TypeScript types would be to hand-author a 26-entry (or, for full Unicode, hopeless) lookup type per direction — which is exactly why the feature is built into the compiler instead. ## The `intrinsic` marker Open `lib.es5.d.ts` and the declarations read: ```ts type Uppercase<S extends string> = intrinsic; type Lowercase<S extends string> = intrinsic; type Capitalize<S extends string> = intrinsic; type Uncapitalize<S extends string> = intrinsic; ``` `intrinsic` is not a type you can use. It is a marker that tells the checker "the implementation of this alias lives in the compiler's own code". Writing `type MyThing<S> = intrinsic;` in application code is rejected with a message stating that the `intrinsic` keyword may only be used to declare compiler-provided intrinsic types. So the set of four is closed: you cannot add a fifth, and any library claiming a `Titlecase` type is implementing it with a hand-written character map or not at all. ## Where they earn their place On their own they are a curiosity. Combined with template literal types they let you derive one naming convention from another: ```ts type Event = "click" | "focus"; type HandlerName = `on${Capitalize<Event>}`; // "onClick" | "onFocus" type Env = Uppercase<`app_${"host" | "port"}`>; // "APP_HOST" | "APP_PORT" ``` That is the shape most real usage takes: a small source vocabulary in one convention, a derived vocabulary in another, kept in sync by the compiler rather than by a comment. ## Compile-time only All four are erased. `Uppercase<"a">` produces no `toUpperCase()` call and no runtime value; if you need the actual uppercased string at runtime you still call the JavaScript method. A frequent confusion is expecting the type to transform data — it only describes what the data must already look like. If a runtime value must match the derived type, the transformation has to exist in the code too, and the type merely checks that it does.

  • What is `Capitalize<'hello world'>` and why does that surprise people?
    It is `"Hello world"`. `Capitalize` uppercases the first character of the whole string literal and nothing else — there is no notion of words in the type system, so no space-splitting happens. Anyone expecting title-casing has confused it with a runtime helper; producing `"Hello World"` would require splitting on a separator with `infer` and recursing.
  • What does `Uppercase<string>` evaluate to?
    It evaluates to `string`. With no literal characters to transform, the compiler cannot narrow anything, and the set of uppercase strings is still described by `string` at that precision. The same holds for the other three. This is worth remembering when a derived type silently degrades to `string` because an input widened somewhere upstream.
  • Could a library ship its own `Titlecase` type using the same mechanism?
    Not using `intrinsic` — that keyword is reserved for the compiler's own library declarations and is an error anywhere else. A library can only approximate case work with a hand-written character-mapping type plus recursion over the string, which is verbose and slow to check. The built-in set of four is effectively closed.

saying these in an interview costs you the question

  • Says Capitalize uppercases the first letter of every word
  • Thinks the types call toUpperCase at runtime
  • Claims you can declare your own intrinsic type
  • Expects Uppercase<string> to be an error
  • Believes they work on values rather than types

context