Advanced Type Manipulation: Core Techniques (Part 1)
Synthesize typeof, keyof, indexed access, conditional types, mapped types, template literal types, and infer into combined, real-world type utilities, reviewing how each building block composes with the others.
By this point you've met typeof, keyof, indexed access types, conditional types, mapped types, template literal types, and infer individually. Each is useful alone, but their real power shows up when you combine them — this lesson is a synthesis, walking through how these pieces snap together to build type-level utilities you'll recognize from TypeScript's own standard library.
Quick recap: the building blocks
typeof (in type position) extracts the static type of a value — typeof someObject gives you the type TypeScript already inferred for someObject, letting you derive a type from a value instead of writing it out by hand. keyof T produces a union of a type's property names as string (or symbol/number) literal types. Indexed access, T[K], looks up the type of the property named by K on T, and works with unions of keys too (T["a" | "b"]).
Conditional types, T extends U ? X : Y, branch at the type level based on whether T is assignable to U. Mapped types, { [K in keyof T]: ... }, build a new object type by iterating over another type's keys and transforming each property. Template literal types build new string literal types by combining literal segments with other types, similar to JS template literals but operating on types.
infer, usable only inside a conditional type's extends clause, introduces a new type variable that the compiler solves for by pattern-matching against the type being checked — it's how you 'pull a type out' of a larger structure, like extracting a function's return type or an array's element type.
Combining typeof with keyof and indexed access
A very common combination is deriving a type directly from a runtime object: const config = { host: "localhost", port: 8080 }; type Config = typeof config; gives you an object type without writing it by hand, and keeps it in sync automatically if config changes. From there, keyof Config gives "host" | "port", and Config["port"] gives number — you've gone from a value, to its type, to its keys, to one specific property's type, using three different tools in sequence.
This chain — value to typeof, to keyof, to indexed access — is the standard way to build type-safe accessors, like a get(config, key) function whose return type depends on which key was passed in, without ever writing the property types out redundantly.
Combining conditional types, mapped types, and infer
Mapped types often use a conditional type inside their value position to transform each property differently depending on its own type — for example, only wrapping function-typed properties in a Promise, and leaving other properties untouched: { [K in keyof T]: T[K] extends (...args: any[]) => infer R ? Promise<R> : T[K] }. This single utility type combines a mapped type (iterating keys), a conditional type (branching per property), and infer (extracting a function's return type) all at once.
infer inside a conditional type is how you write your own version of utilities like ReturnType<T>. ReturnType<T> = T extends (...args: any[]) => infer R ? R : never reads as: 'if T matches the shape of a function, capture whatever its return type is into R, and give that back; otherwise, give back never since T isn't callable at all.' The extends clause here isn't really about subtyping — it's a pattern to match against, with infer R as the placeholder for the piece you want extracted.
Template literal types combine naturally with keyof and mapped types too: { [K in keyof T as on${Capitalize<string & K>}]: () => void } remaps every key of T to an event-handler-style name (onClick, onHover, ...) using a template literal type inside a mapped type's key-remapping clause — three techniques working together to generate an entirely new set of property names from an existing type.
A hand-written ReturnType using infer
typeof getUser turns the function value into its type: () => { id: number; name: string }. The conditional type then pattern-matches that function type, and infer R captures its return type into R, which is what the whole expression evaluates to. If T isn't a function type at all, the extends check fails and the type resolves to never instead.
type MyReturnType<T> = T extends (...args: any[]) => infer R
? R
: never;
function getUser() {
return { id: 1, name: "Ada" };
}
type UserFromFn = MyReturnType<typeof getUser>;
// { id: number; name: string }Code Examples
get's return type isn't hardcoded — it's Config[K], so TypeScript computes the exact return type for each call based on which literal key was passed, using the value's inferred shape as the single source of truth. Expected result: port: 8080 (typed number), host: "localhost" (typed string)
The mapped type walks every key of Api. For each one, a conditional type checks whether that property is a function; if so, infer A and infer R capture its parameters and return type so the property can be rewritten to return a Promise of the original result, while non-function properties like version pass through unchanged. This is essentially a simplified version of the pattern real libraries use to turn a synchronous API surface into an asynchronous one while preserving every method's parameter types exactly.
T[K] extends (...args: infer A) => infer R ? (...args: A) => Promise<R> : T[K]
const config = {
host: "localhost",
port: 8080,
debug: true
};
type Config = typeof config;
type ConfigKey = keyof Config; // "host" | "port" | "debug"
type PortType = Config["port"]; // number
function get<K extends ConfigKey>(key: K): Config[K] {
return config[key];
}
const port = get("port"); // typed as number
const host = get("host"); // typed as stringtype Promisify<T> = {
[K in keyof T]: T[K] extends (...args: infer A) => infer R
? (...args: A) => Promise<R>
: T[K];
};
interface Api {
getUser(id: number): { id: number; name: string };
version: string;
}
type AsyncApi = Promisify<Api>;
// {
// getUser(id: number): Promise<{ id: number; name: string }>;
// version: string;
// }How the tools compose
Each tool takes the output of the previous one as its input — that's what makes chains like typeof -> keyof -> indexed access -> conditional type possible.
value
| typeof
v
type ---keyof---> union of key literals
| |
| indexed access <--------'
v
property type
|
| conditional type (T extends U ? X : Y)
| with infer capturing a sub-part
v
new derived typeImportant Rules
Prefer deriving types from values with typeof over duplicating a type declaration by hand whenever a representative value already exists — it keeps the type and the value from drifting apart.
infer is only legal inside the extends clause of a conditional type — it can't be used standalone; think of it as introducing a variable that the conditional type's pattern-matching solves for.
When a mapped type's transformation needs to differ per property based on that property's own type, nest a conditional type inside the mapped type's value position.
Chains of these techniques can get hard to read quickly — introduce a named intermediate type alias for each meaningful step rather than writing one enormous nested expression.
This is how TypeScript's own utility types are built
ReturnType<T>, Parameters<T>, InstanceType<T>, and Awaited<T> — all shipped in TypeScript's standard library of global utility types — are implemented with exactly this combination of conditional types and infer. Recognizing the pattern means you can read their source, and more importantly, write your own project-specific equivalents when the built-in set doesn't cover what you need.
type Return<T> = T extends () => infer R ? R: never; // only matches zero-argument functions type Return<T> = T extends (...args: any[]) => infer R ? R: never; // matches functions with any parameter list
Using () in the pattern only matches functions that take no arguments at all; (...args: any[]) matches any function regardless of its parameter list, which is what you almost always want when extracting a return type generically.
[multiple-choice] What does infer R do inside T extends (...args: any[]) => infer R ? R : never? A. Declares a runtime variable named R B. Introduces a type variable that captures the function's return type during the pattern match C. Forces T to be a function D. Has no effect unless combined with keyof
B. Introduces a type variable that captures the function's return type during the pattern match — infer introduces a placeholder type variable that the compiler solves for by matching the conditional type's pattern against the actual type — here it captures whatever the function's return type turns out to be.
[multiple-choice] Given const config = { port: 8080 }; type Config = typeof config;, what does Config["port"] evaluate to? A. "port" B. 8080 as a literal type C. number D. any
C. number — Indexed access looks up the property's inferred type on the object type — typeof config infers port as number (not the literal 8080, since it's a mutable let/object property, not a const primitive with as const), so Config["port"] is number.
[true-false] A mapped type can use a conditional type inside its value position to transform different properties differently based on each property's own type. A. True B. False
A. True — This is exactly the Promisify<T> pattern shown in this lesson — nesting T[K] extends ... ? ... : ... inside a mapped type lets each property be transformed based on its own shape.
Write a type FirstArg<T> that, given a function type, resolves to the type of its first parameter (or never if T isn't a function, or the function takes no parameters). Test it against (id: number, name: string) => void.
type FirstArg<T> = T extends (first: infer A, ...rest: any[]) => any ? A : never; type Test = FirstArg<(id: number, name: string) => void>; // number
Pattern-match with T extends (first: infer A, ...rest: any[]) => any ? A : never.