Uncapitalize<StringType>
Lowercases the first character of a string literal type, leaving the rest untouched.
type Uncapitalize<S extends string> = intrinsic;- Added in TypeScript 4.1
Parameters
| Parameter | Type | Description |
|---|---|---|
$S | string | A string literal type or a union of them. Only the first character is affected. |
Return value
A string literal type with the first character lowercasedThe literal type with only its first character lowercased. Distributes over unions; the empty string literal is returned unchanged.
Errors
tsc errors if the argument does not satisfy the `string` constraint.
Changelog
- PHP 4.1Introduced with template literal types. Implemented as a compiler intrinsic using `str.charAt(0).toLowerCase() + str.slice(1)`.
Uncapitalize is the inverse of Capitalize: first character down, rest untouched. On its own it is unremarkable. Paired with infer in a template literal type it becomes the tool for going backwards from a method name to a property name.
The pattern is: match the prefix, capture the remainder, fix its casing. S extends `get${infer R}` ? Uncapitalize<R> : never turns "getName" into "name". That single conditional is what lets you derive a state shape from an accessor interface, recover prop names from onClick-style handlers, or map a class's handleSubmit methods back to the events they respond to — without maintaining the second list by hand.
What trips people up
infer Rafter a prefix gives you the raw remainder, which is still capitalised. Forgetting theUncapitalizeleaves you with"Name", and the resulting object keys silently do not match your data.- It is not the exact inverse of
Capitalizefor every input — a string that already begins lowercase, or with a digit, comes back unchanged, so the round trip is lossy. - Only the first character changes.
Uncapitalize<"HTTPServer">is"hTTPServer", which is almost never what an acronym-heavy codebase wants.
Example
When to use it
- Derive a state object's field names from a union of `getX` accessor names.
- Recover prop names from `onEvent`-style handler keys with `infer` plus a prefix match.
- Map class method names such as `handleSubmit` back to the action names they handle.
Frequently asked questions
How do I turn getName into name at the type level?
Match and capture the remainder, then lowercase its first character: `S extends `get${infer R}` ? Uncapitalize<R> : never`. Without the `Uncapitalize` you would get `"Name"` with the capital intact.
Is Uncapitalize the exact inverse of Capitalize?
Not always. It only changes the first character, so a literal that already starts lowercase or with a non-letter round-trips unchanged, and acronyms like `"HTTPServer"` become `"hTTPServer"` rather than `"httpServer"`.
Does Uncapitalize work on a union of string literals?
Yes — it distributes over the union, so `Uncapitalize<"Name" | "Email">` is `"name" | "email"`. That is what makes it usable in the key-remapping clause of a mapped type.
More examples
Round-tripping props to handler names and back
`Capitalize` builds the handler keys and `Uncapitalize` recovers the original event union from them, so the two types stay in sync from a single source. Output: `[ 'onClick', 'onSubmit' ]` then `clicked submit`.
type Event = "click" | "submit";
type HandlerName<E extends string> = `on${Capitalize<E>}`;
type EventOf<H extends string> = H extends `on${infer R}` ? Uncapitalize<R> : never;
type Handlers = { [E in Event as HandlerName<E>]: () => string };
const handlers: Handlers = {
onClick: () => "clicked",
onSubmit: () => "submitted",
};
const back: EventOf<keyof Handlers & string> = "submit"; // "click" | "submit"
console.log(Object.keys(handlers).sort());
console.log(handlers.onClick(), back);Building a state shape from an accessor interface
`State<Store>` resolves to `{ userId: number; theme: "light" | "dark" }` — `reset` is filtered out because it does not match the `get` prefix, and each value type comes from the accessor's return type. Output: `42 dark` then `[ 'theme', 'userId' ]`.
interface Store {
getUserId(): number;
getTheme(): "light" | "dark";
reset(): void;
}
type AccessorKeys<T> = {
[K in keyof T & string]: K extends `get${string}` ? K : never;
}[keyof T & string];
type State<T> = {
[K in AccessorKeys<T> as K extends `get${infer R}` ? Uncapitalize<R> : never]:
T[K] extends () => infer V ? V : never;
};
const state: State<Store> = { userId: 42, theme: "dark" };
console.log(state.userId, state.theme);
console.log(Object.keys(state).sort());
Discussion