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.
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.
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
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.
// 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
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: 25Explain 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.
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 Completion is saved in this browser only. It does not measure mastery.