Utility Types & Typed Callbacks

The Course
•
5 min read

A reusable type helper is often called a utility type. You already used Record and Readonly. Pick<Session, "topic" | "minutes"> selects named fields from Session. Partial<Session> makes its immediate properties optional. These transformations affect checking; they do not pick or remove keys from a runtime object.

Choose the narrowest operation contract. If a function only changes done, a patch type containing only done is clearer than Partial<Session>, which permits topic and minutes too. Make done required when the operation must receive it; make it optional only for an update that may leave it alone. Making everything optional can hide missing required data, so avoid using Partial as a generic response to every diagnostic.

A function can accept another function. In (session: Session) => string, the arrow describes a function type with a Session parameter and string return. It is a type annotation, distinct from the executable arrow function you pass as an argument.

When the parameter expects that callback type, the callback’s own parameter is inferred. For example, format(session, item => item.topic) knows item is Session. This is contextual typing: nearby code supplies the contract.

The beginner goal is to read and use these small relationships, not to write elaborate type puzzles. Advanced mapped and conditional types can wait until a real reusable API needs them.

Predict, check, then run

Before running, write down the output and explain which information the checker knows. Click Check types first, then Check & run.

Predict, then runTypeScript
type Session = {
  topic: string;

  minutes: number;

  done: boolean;
};

type Preview = Pick<Session, "topic" | "minutes">;

function format(session: Preview, render: (item: Preview) => string): string {
  return render(session);
}

console.log(
  format(
    { topic: "Types", minutes: 25 },
    item => `${item.topic}: ${item.minutes}`
  )
);

Enable JavaScript to edit and type-check this example.

Check your prediction
Types: 25

Read a real compiler error

The helper only selects keys that actually exist on Session.

Find and fix the type errorTypeScript
type Session = {
  topic: string;

  minutes: number;

  done: boolean;
};

const preview: Pick<Session, "topic" | "duration"> = {
  topic: "Types",
  duration: 25
};

console.log(preview);

Enable JavaScript to edit and type-check this example.

Check your diagnosis

Use minutes as the selected key and the runtime property name. Pick cannot rename minutes into duration; that would require a new shape and an explicit runtime transformation.

Your turn

Exercise

Utility Types & Typed Callbacks: practise

Define Session and a patch type using Pick<Session, “done”>. Write complete(session, patch) returning a copied Session. It must preserve topic and minutes. Print true for the updated record and false for the original. Also pass a patch variable that contains extra topic and minutes properties: the updated record must still preserve the original topic and minutes. A narrow parameter type does not strip extra keys at runtime.

Your workspaceTypeScript
// Use a narrow required done patch, not a patch that can alter every field.

Enable JavaScript to edit and type-check this example.

Hint 1: the idea

Pick selects a property in the type system; it does not remove other runtime fields. Copy the session and explicitly assign the one field this operation is allowed to change.

Hint 2: a first step

The function needs Session and Pick<Session, “done”> parameters, then a Session return.

Show one solution
One possible solutionTypeScript
type Session = {
  topic: string;

  minutes: number;

  done: boolean;
};

function complete(session: Session, patch: Pick<Session, "done">): Session {
  return { ...session, done: patch.done };
}

const original: Session = { topic: "Types", minutes: 25, done: false };
const updated = complete(original, { done: true });

console.log(updated.done);
console.log(original.done);

// A variable can carry extra fields even when the parameter type is narrower.
const widerPatch = { done: true, topic: "Wrong", minutes: 999 };
const safe = complete(original, widerPatch);

console.log(`${safe.topic}: ${safe.minutes}`);

Enable JavaScript to edit and type-check this example.

true
false
Types: 25

Explain the types at the function boundary and the runtime checks, if any. Compare your approach before copying this answer.

Change it and explain it

Why is spreading the whole patch unsafe in the widerPatch case? Temporarily try it and check that the preserved-field expectation fails. Then restore the explicit done assignment. For an optional patch, Partial<Pick<Session, "done">> permits an empty object: use done: patch.done ?? session.done so absence preserves the original while false remains meaningful. Explain why this makes sense for an optional update but not for creating a complete Session. Try a formatter callback returning a number where string was promised and inspect its diagnostic.

Going further: an optional update

For an update that may omit done, combine Partial and Pick. ?? keeps a supplied false while using the original only when the patch omits done. Predict both outcomes; using || here would incorrectly replace false.

Optional patch without losing falseTypeScript
type Session = {
  topic: string;

  minutes: number;

  done: boolean;
};

type DonePatch = Partial<Pick<Session, "done">>;

function update(session: Session, patch: DonePatch): Session {
  return { ...session, done: patch.done ?? session.done };
}

const original: Session = { topic: "Types", minutes: 25, done: true };

console.log(update(original, {}).done);
console.log(update(original, { done: false }).done);

Enable JavaScript to edit and type-check this example.

Check your prediction
true
false

Was this article helpful?

Your response is saved on this device.