On Your Own: Settings Patches

The Course
•
4 min read

Build a settings patch boundary without copying the event gateway. Settings has readonly enabled:boolean and minutes:number. Derive Patch from a mapped type so each key is paired with its own value. Decode unknown patches explicitly: enabled requires boolean; minutes requires a finite integer from 0 through 180. Reject unknown keys and do not coerce text.

Apply a decoded patch by constructing a new Settings record. The original must stay unchanged. Extra input properties should not enter the resulting settings. Preserve zero and false as meaningful values rather than replacing them with truthy defaults.

Use about twenty minutes before looking at the worked answer. Test both valid branches, missing values, the two numeric bounds, fractional/text values and unknown keys. This is a fresh API with no event subscriptions, transport or brand requirement. Explain which relationships the mapped union checks and which rules the decoder implements.

Predict, check, and run

Write the output before running. Use Check types first, then Check & run. Types disappear at runtime: explain both what the compiler checks and what the code actually does. Each editor is an independent ES module; await asynchronous work explicitly.

Predict, then runTypeScript
type Settings = {
  readonly enabled: boolean;

  readonly minutes: number;
};

type Patch = {
  [K in keyof Settings]: {
    key: K;

    value: Settings[K];
  };
}[keyof Settings];

const patch: Patch = { key: "enabled", value: false };

console.log(patch.key, patch.value);

Enable JavaScript to edit and type-check this example.

Check your prediction
enabled false

Build it yourself

Exercise

On Your Own: Settings Patches: practise

Implement PatchOf<S>, decodePatch and apply. From {enabled:true,minutes:25}, apply enabled:false and minutes:0 without changing the source. Print false 0 and true 25. Assert text minutes, out-of-range counts, missing values and unknown keys fail; print invalid patches rejected.

Your workspaceTypeScript
// Start from the stated contract. Add a valid call and an edge case.

Enable JavaScript to edit and type-check this example.

Hint 1: the idea

Derive the correlated union, but validate each supported branch explicitly at runtime.

Hint 2: a first step

Narrow patch.key before assigning a value in a new settings object.

Show one solution
One possible solutionTypeScript
type Settings = {
  readonly enabled: boolean;

  readonly minutes: number;
};

type PatchOf<S> = {
  [K in keyof S]: {
    key: K;

    value: S[K];
  };
}[keyof S];

type Patch = PatchOf<Settings>;

type Result<T> =
  | {
      ok: true;

      value: T;
    }
  | {
      ok: false;

      message: string;
    };

function decodePatch(raw: unknown): Result<Patch> {
  if (
    typeof raw !== "object" ||
    raw === null ||
    !("key" in raw) ||
    !("value" in raw)
  ) {
    return { ok: false, message: "Expected patch" };
  }

  if (raw.key === "enabled" && typeof raw.value === "boolean") {
    return { ok: true, value: { key: "enabled", value: raw.value } };
  }

  if (
    raw.key === "minutes" &&
    typeof raw.value === "number" &&
    Number.isFinite(raw.value) &&
    Number.isInteger(raw.value) &&
    raw.value >= 0 &&
    raw.value <= 180
  ) {
    return { ok: true, value: { key: "minutes", value: raw.value } };
  }

  return { ok: false, message: "Invalid patch" };
}

function apply(settings: Settings, patch: Patch): Settings {
  if (patch.key === "enabled") {
    return { enabled: patch.value, minutes: settings.minutes };
  }

  return { enabled: settings.enabled, minutes: patch.value };
}

const source: Settings = { enabled: true, minutes: 25 };
const enabled = decodePatch({ key: "enabled", value: false, secret: true });
const minutes = decodePatch({ key: "minutes", value: 0 });

if (!enabled.ok || !minutes.ok) {
  throw new Error("Valid patch rejected");
}

const updated = apply(apply(source, enabled.value), minutes.value);

if (
  updated === source ||
  "secret" in updated ||
  source.enabled !== true ||
  source.minutes !== 25
) {
  throw new Error("Ownership failed");
}

console.log(updated.enabled, updated.minutes);
console.log(source.enabled, source.minutes);

for (const raw of [
  null,
  { key: "enabled" },
  { key: "other", value: true },
  { key: "enabled", value: "false" },
  ...[-1, 181, 1.5, "0", Infinity, NaN].map(value => ({
    key: "minutes",
    value
  }))
]) {
  if (decodePatch(raw).ok) {
    throw new Error("Invalid patch accepted");
  }
}

if (!decodePatch({ key: "minutes", value: 180 }).ok) {
  throw new Error("Upper bound rejected");
}

console.log("invalid patches rejected");

Enable JavaScript to edit and type-check this example.

false 0
true 25
invalid patches rejected

Trace the implementation against the task before comparing styles. A passing example supports that case; use the changes below to challenge the wider contract.

Change it and test it

Add a public negative caller pairing enabled with a number. Remove a runtime bound and prove the corresponding test fails. Then explain whether adding a third setting should require an explicit decoder branch.

Was this article helpful?

Your response is saved on this device.