In this second episode of our TotalTypeScript-inspired series, we dive deep into Intermediate TypeScript. We cover Indexed Access Types, keyof and typeof operators, built-in utility types (Pick, Omit, Record, Extract, Exclude), Mapped Types, and Generics with constraints (extends).
1. Indexed Access Types, keyof, and typeof#
Indexed Access Types allow you to look up specific properties on another type using dictionary syntax:
type UserProfile = {
id: string;
name: string;
settings: {
theme: "light" | "dark";
notifications: boolean;
};
};
// Indexed Access Type: Extract nested property type
type Theme = UserProfile["settings"]["theme"]; // "light" | "dark"
// keyof Operator: Extract union of object keys
type UserProfileKeys = keyof UserProfile; // "id" | "name" | "settings"
// typeof Operator: Capture type of runtime object
const defaultSettings = {
env: "production",
retries: 3,
debug: false,
};
type AppSettings = typeof defaultSettings;2. Generics and Generic Constraints (extends)#
Generics allow functions, interfaces, and classes to accept type parameters dynamically while retaining type relationships.
// Simple Generic Function
function identity<T>(value: T): T {
return value;
}
const num = identity(42); // T inferred as number
const str = identity("hello"); // T inferred as string
Constraining Generics with extends#
By default, generic type parameter <T> can be anything. Use T extends SomeType to enforce type boundaries:
interface HasId {
id: string;
}
// Generic Constraint: T must have an 'id' property
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find((item) => item.id === id);
}
const users = [
{ id: "u1", name: "Alice" },
{ id: "u2", name: "Bob" },
];
const foundUser = findById(users, "u1"); // Returns { id: string; name: string }
Generic Defaults#
Assign default types to generic parameters for ergonomics:
interface FetchResponse<TData = unknown> {
data: TData;
status: number;
error?: string;
}
// Default type is unknown
const rawResponse: FetchResponse = { data: "raw", status: 200 };
// Explicit type payload
const userResponse: FetchResponse<{ name: string }> = {
data: { name: "Rachmat" },
status: 200,
};3. Built-In Utility Types Deep Dive#
TypeScript provides built-in type helpers for transforming object shapes and function signatures:
interface User {
id: string;
name: string;
email: string;
role: "admin" | "user" | "guest";
createdAt: Date;
}
// 1. Pick: Select subset of keys
type UserPreview = Pick<User, "id" | "name">;
// 2. Omit: Remove subset of keys
type UserCreationDto = Omit<User, "id" | "createdAt">;
// 3. Record: Construct object map type
type UserRolePermissions = Record<User["role"], string[]>;
const permissions: UserRolePermissions = {
admin: ["create", "read", "update", "delete"],
user: ["read", "update"],
guest: ["read"],
};
// 4. Extract and Exclude for Unions
type AdminOrUser = Extract<User["role"], "admin" | "user">; // "admin" | "user"
type NonGuestRole = Exclude<User["role"], "guest">; // "admin" | "user"
Function Utility Helpers (ReturnType & Parameters)#
function createUser(name: string, email: string) {
return { id: "usr_99", name, email, createdAt: new Date() };
}
// Extract return type of function
type CreatedUser = ReturnType<typeof createUser>;
// Extract parameters tuple of function
type CreateUserParams = Parameters<typeof createUser>; // [name: string, email: string]
4. Mapped Types: Creating Dynamically Derived Object Shapes#
Mapped Types iterate over key unions to build new object types:
type FeatureFlags = {
darkMode: boolean;
newDashboard: boolean;
betaAccess: boolean;
};
// Mapped Type: Transform all properties to boolean getters
type FeatureGetters = {
[K in keyof FeatureFlags]: () => FeatureFlags[K];
};
const getters: FeatureGetters = {
darkMode: () => true,
newDashboard: () => false,
betaAccess: () => true,
};Property Modifiers (-readonly, ?)#
Use + or - prefixes to add or remove modifiers like readonly or ?:
type ReadonlyUser = Readonly<User>;
// Remove readonly modifier to make object mutable again
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
type UnlockedUser = Mutable<ReadonlyUser>;
// Make all properties required by removing ? modifier
type Concrete<T> = {
[K in keyof T]-?: T[K];
};5. Key Remapping with as Clause in Mapped Types#
Filter or rename keys dynamically during mapped type execution:
type Events = {
click: { x: number; y: number };
hover: { elementId: string };
submit: { formData: Record<string, string> };
};
// Remap key names: 'click' -> 'onClick'
type EventHandlers = {
[K in keyof Events as `on${Capitalize<K>}`]: (event: Events[K]) => void;
};
const handlers: EventHandlers = {
onClick: (e) => console.log(e.x, e.y),
onHover: (e) => console.log(e.elementId),
onSubmit: (e) => console.log(e.formData),
};Key Takeaways#
- Leverage
keyofand Indexed Access: UseT[K]andkeyof Tto bind parameter types dynamically to object key selections. - Constrain Generics: Use
T extends HasPropertyto ensure generic type parameters satisfy required interfaces before accessing properties. - Use Mapped Types for Transformations: Use
[K in keyof T]withasclause key remapping to generate getters, setters, or event handlers without manual repetition. - Master Utility Types: Use
Pick,Omit,Record,ReturnType, andParametersto derive types directly from authoritative functions and domain models.

