Technology  /  TypeScript

๐Ÿ”ท TypeScript 16 guides ยท updated 2026

Type erasure, structural typing, generics, and the type-level programming toolkit โ€” TypeScript taught the way it actually behaves at compile time and at runtime.

Template Literal Types, keyof and typeof in TypeScript Explained

keyof and typeof have appeared in earlier guides in this series as supporting tools โ€” extracting a key union from an object type, converting a valueโ€™s type into something usable in a type position. This guide slows down on both, then covers template literal types, which let you build string types the same way JavaScript template literals build string values: `get${Capitalize<K>}` as a type, not just a runtime string, checked entirely at compile time.


typeof in a Type Position

const config = {
apiUrl: "https://api.example.com",
timeout: 5000,
retries: 3,
};
type Config = typeof config;
// { apiUrl: string; timeout: number; retries: number } โ€” derived, not hand-written

This is a different typeof from the runtime operator used in narrowing (typeof value === "string") โ€” same keyword, but TypeScript overloads it to mean something different depending on where it appears. In a type position (after a : or inside a type declaration), typeof someValue produces the type of that value, inferred exactly as TypeScript would infer it from the declaration itself. This is the standard way to derive a type from a value you already have, rather than writing the type by hand and risking the two drifting out of sync.

// Deriving a type from a function, not just a plain object
function createUser(name: string, age: number) {
return { name, age, id: crypto.randomUUID() };
}
type NewUser = ReturnType<typeof createUser>;
// typeof createUser gives the function's TYPE; ReturnType then extracts what it returns

typeof createUser is necessary here because ReturnType<T> expects a function type as its argument, and createUser on its own refers to the function value โ€” you canโ€™t pass a value where a type is expected, so typeof performs that conversion.


keyof โ€” Turning an Object Type Into a Union of Its Keys

interface User {
id: string;
name: string;
age: number;
}
type UserKeys = keyof User; // "id" | "name" | "age"
function getProperty(user: User, key: keyof User) {
return user[key];
}
getProperty(someUser, "name"); // fine
getProperty(someUser, "email"); // Error: "email" is not a key of User

keyof T produces a literal union of Tโ€™s property names as strings (or numbers, for numerically-keyed types like arrays) โ€” this is what makes getProperty above safer than accepting a plain string for key: only real property names of User are ever valid arguments, checked at compile time rather than discovered as undefined at runtime when someone passes a typoโ€™d key.

// keyof combined with typeof โ€” a very common real pairing
const defaultSettings = {
theme: "light",
fontSize: 14,
notifications: true,
};
type SettingKey = keyof typeof defaultSettings; // "theme" | "fontSize" | "notifications"
function updateSetting<K extends SettingKey>(key: K, value: typeof defaultSettings[K]) {
defaultSettings[key] = value;
}

keyof typeof someObject is one of the most common combinations in real TypeScript code: get the type of an existing runtime object (typeof), then get the union of its keys (keyof) โ€” useful whenever you have a concrete configuration object and want a type that always stays in sync with it, rather than a separately maintained literal union that could drift.


Template Literal Types โ€” String Patterns as Types

type Greeting = `Hello, ${string}`;
const a: Greeting = "Hello, Alice"; // fine โ€” matches the pattern
const b: Greeting = "Hey, Alice"; // Error: doesn't start with "Hello, "

A template literal type looks exactly like a JavaScript template literal, but written in a type position โ€” ${string} here is a placeholder meaning โ€œany string value,โ€ and the type only accepts strings matching the literal parts exactly ("Hello, " at the start) with the placeholder filling the rest.

// Combining literal unions inside a template literal type โ€” this is where it gets genuinely powerful
type Size = "small" | "medium" | "large";
type Color = "red" | "blue" | "green";
type ButtonClass = `btn-${Size}-${Color}`;
// "btn-small-red" | "btn-small-blue" | "btn-small-green" | "btn-medium-red" | ... (9 combinations total)
const cls: ButtonClass = "btn-medium-blue"; // fine
const bad: ButtonClass = "btn-huge-blue"; // Error: "huge" isn't a valid Size

TypeScript expands every combination of the unions inside the template automatically โ€” two unions with 3 members each produce all 9 valid combined strings as the resulting type, and any string outside those 9 exact combinations is rejected. This is exactly how libraries generating typed CSS class names, typed route paths, or typed event names validate string patterns at compile time instead of only at runtime.


Practical Pattern: Typed Event Names

type EventName = "click" | "hover" | "focus";
type EventHandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onHover" | "onFocus"
interface Props {
onClick?: () => void;
onHover?: () => void;
onFocus?: () => void;
}
// Generating this mapping automatically instead of hand-writing each property
type EventHandlers<E extends string> = {
[K in E as `on${Capitalize<K>}`]?: () => void;
};
type ButtonProps = EventHandlers<EventName>;
// Same as Props above, but derived from EventName โ€” stays in sync if EventName changes

This combines a mapped typeโ€™s key remapping (from the previous guide) with a template literal type โ€” the as clause builds each new key using the same `on${Capitalize<K>}` pattern, applied automatically across every member of EventName, rather than manually writing onClick, onHover, and onFocus as three separate properties that could fall out of sync with the underlying EventName union.


Built-In String Manipulation Types

type UppercaseGreeting = Uppercase<"hello">; // "HELLO"
type LowercaseGreeting = Lowercase<"HELLO">; // "hello"
type CapitalizedName = Capitalize<"alice">; // "Alice"
type UncapitalizedName = Uncapitalize<"Alice">; // "alice"

These four are TypeScriptโ€™s only built-in intrinsic string manipulation types โ€” implemented directly in the compiler (not expressible in ordinary TypeScript the way mapped/conditional types are), specifically because they need to operate on the literal characters of a string type, which requires compiler-level support rather than being derivable from other type-system features.


Practical Pattern: Type-Safe Object Path Strings

type PathKeys<T, Prefix extends string = ""> = {
[K in keyof T & string]: T[K] extends object
? PathKeys<T[K], `${Prefix}${K}.`>
: `${Prefix}${K}`;
}[keyof T & string];
interface Settings {
user: {
name: string;
address: {
city: string;
};
};
theme: string;
}
type SettingsPath = PathKeys<Settings>;
// "user.name" | "user.address.city" | "theme"
function getByPath(settings: Settings, path: SettingsPath) { ... }
getByPath(settings, "user.address.city"); // valid
getByPath(settings, "user.email"); // Error: not a valid path

This recursive combination of a mapped type, a conditional type, and a template literal type produces every valid dot-notation path through a nested object as a literal union โ€” the same technique that powers type-safe form libraries (validating "user.address.city"-style field paths against your actual data shape) and typed i18n key lookups. Itโ€™s also a useful worked example of how the constructs from this whole โ€œadvanced typesโ€ section โ€” generics, conditional types, infer, mapped types, and template literals โ€” compose together in real, non-toy code.

T[K] is an object

T[K] is not an object

PathKeys<Settings>

For each key K in Settings

Recurse: PathKeys<T[K], prefix + K + '.'>

Produce: prefix + K

Union all results together


keyof With Index Signatures

interface StringMap {
[key: string]: number;
}
type Keys = keyof StringMap; // string | number โ€” JS object keys are always coercible from number to string

A subtlety worth knowing: keyof on a type with a string index signature produces string | number, not just string โ€” because in actual JavaScript, numeric object keys are always converted to strings under the hood (obj[1] and obj["1"] access the same property), and TypeScriptโ€™s keyof reflects that real runtime behavior rather than pretending numeric keys donโ€™t exist.


Extracting Parts of a String Type With infer

type ExtractRouteParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param | keyof ExtractRouteParams<Rest>]: string }
: T extends `${string}:${infer Param}`
? { [K in Param]: string }
: {};
type Params = ExtractRouteParams<"/users/:userId/posts/:postId">;
// { userId: string; postId: string }
function buildUrl(path: string, params: Params) { ... }

This combines infer inside a template literal pattern (introduced generally in the mapped and conditional types guide) with recursion โ€” matching against a literal string pattern (`${string}:${infer Param}/${infer Rest}`) to pull out one route parameter at a time, then recursing on whateverโ€™s left after the next /. This is essentially how typed routing libraries (tRPC, some versions of React Routerโ€™s typed hooks) derive parameter types directly from a route string literal, so a typoโ€™d or missing parameter in buildUrlโ€™s second argument is caught at compile time instead of producing a broken URL at runtime.

// A simpler, non-recursive example: splitting a "key.value" string into a tuple
type SplitOnce<T extends string, Sep extends string> =
T extends `${infer Left}${Sep}${infer Right}` ? [Left, Right] : [T, never];
type Parts = SplitOnce<"user.name", ".">; // ["user", "name"]

When Template Literal Types Cost More Than Theyโ€™re Worth

// A union with many combined variants can grow very large very fast
type Size = "xs" | "sm" | "md" | "lg" | "xl";
type Color = "red" | "blue" | "green" | "yellow" | "purple" | "gray";
type Variant = "solid" | "outline" | "ghost";
type AllClasses = `btn-${Size}-${Color}-${Variant}`;
// 5 ร— 6 ร— 3 = 90 distinct string literal members in this one type

Each additional union multiplies the total number of literal combinations TypeScript has to track โ€” three unions of modest size already produce 90 distinct types, and this can measurably slow down type-checking (and editor autocomplete responsiveness) once combinations reach the thousands. If a generated union grows unreasonably large, consider whether the string actually needs full compile-time validation, or whether a looser type (plain string, validated at runtime instead) is the more practical trade-off for that specific case.

Frequently Asked Questions

Is typeof in TypeScript the same as JavaScriptโ€™s runtime typeof operator? Same keyword, two different meanings depending on context. In a value position (inside an if, a condition, an expression), itโ€™s the ordinary JavaScript runtime operator used for narrowing. In a type position (after a colon, inside a type alias), itโ€™s TypeScriptโ€™s compile-time โ€œgive me this valueโ€™s typeโ€ operator. Context disambiguates which one applies.

Can template literal types validate arbitrary string formats, like email addresses or URLs? Not in general โ€” template literal types work by expanding literal union combinations, so theyโ€™re well-suited to a finite set of known patterns (like the ButtonClass example), but they canโ€™t express a genuinely open-ended pattern like โ€œany valid email address,โ€ since that would require regex-style matching the type system doesnโ€™t support. For that, runtime validation (Zod, a custom validator) is still the right tool.

Does keyof include inherited properties from an extended interface? Yes โ€” keyof on an interface that extends another includes the parentโ€™s keys too, since the extending interfaceโ€™s full resolved shape includes everything the parent requires.

Why would I use keyof typeof obj instead of just writing the key union by hand? Because it stays automatically in sync โ€” if obj gains or loses a property, keyof typeof obj picks up the change immediately, whereas a hand-written union ("theme" | "fontSize" | ...) has to be manually updated every time and can silently drift out of sync with the actual object.

Is deeply recursive type-level programming like the PathKeys example something Iโ€™ll write often? Rarely for typical application code, but recognizing the pattern is valuable โ€” itโ€™s exactly how several popular typed form/routing/i18n libraries implement their compile-time safety, and understanding it means you can read (and debug) their type definitions when something doesnโ€™t type-check the way you expect.


Whatโ€™s Next

This closes out the advanced type-system section โ€” generics, utility types, mapped and conditional types, and now template literal types together cover TypeScriptโ€™s full type-level programming toolkit. Next: Classes in TypeScript, moving from pure type-level constructs back to the object-oriented features โ€” access modifiers, abstract classes, and inheritance โ€” that generate real runtime code.