Skip to main content

TS Ep 2: Intermediate TypeScript — Generics, Type Helpers, and Mapped Types

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
typescript - This article is part of a series.
Part 2: This Article
Generics and type transformations are the engine room of production TypeScript codebases. They allow you to write reusable, type-safe functions and object transformations without duplicating type definitions.

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
#

  1. Leverage keyof and Indexed Access: Use T[K] and keyof T to bind parameter types dynamically to object key selections.
  2. Constrain Generics: Use T extends HasProperty to ensure generic type parameters satisfy required interfaces before accessing properties.
  3. Use Mapped Types for Transformations: Use [K in keyof T] with as clause key remapping to generate getters, setters, or event handlers without manual repetition.
  4. Master Utility Types: Use Pick, Omit, Record, ReturnType, and Parameters to derive types directly from authoritative functions and domain models.
typescript - This article is part of a series.
Part 2: This Article