TypeDrop
2026-03-27 Challenge
2026-03-27
Easy
Typed Student Grade Book Aggregator
You're building the reporting module for an online learning platform. Teachers submit raw grade entries for students across multiple subjects, and your aggregator must validate the entries, compute per-student summaries, and assign letter grades — all with fully typed inputs and outputs.
Goals
- Define a `ValidScore` branded type and a `ValidatedEntry` mapped type that swaps in the brand without re-listing every field.
- Implement `toValidScore` to safely narrow a raw `number` into `ValidScore | null` using a range check — no type assertions.
- Implement `toLetterGrade` that accepts only a `ValidScore` parameter and returns the correct `LetterGrade` band.
- Implement `aggregateGrades` to validate all entries, group by student, compute sorted subjects and rounded averages, and return a typed `GradeBookResult`.
challenge.ts
/** A score confirmed to be in [0, 100]. */
type ValidScore = number & { readonly __brand: "ValidScore" };
/** ValidatedEntry — same as RawGradeEntry but score is ValidScore. */
type ValidatedEntry = Omit<RawGradeEntry, "score"> & { score: ValidScore };
type LetterGrade = "A" | "B" | "C" | "D" | "F";
interface StudentSummary {
studentId: string;
studentName: string;
subjects: string[]; // unique, sorted A→Z
averageScore: number; // rounded to 2 dp
letterGrade: LetterGrade;
}
interface GradeBookResult {
summaries: StudentSummary[];
errors: ValidationError[];
}
// Main function signature:
function aggregateGrades(entries: RawGradeEntry[]): GradeBookResult { ... }
Hints (click to reveal)
Hints
- For the branded type, intersect `number` with a readonly object literal: `number & { readonly __brand: 'ValidScore' }` — then use an explicit cast only inside `toValidScore` where you've already verified the range.
- For `ValidatedEntry`, reach for `Omit<RawGradeEntry, 'score'> & { score: ValidScore }` to surgically replace just the one field.
- When computing `averageScore` (a plain `number`) before calling `toLetterGrade`, you'll need to pass it through `toValidScore` first — think about what to do if it somehow returns `null` (hint: a clamped average can't be out of range, but TypeScript doesn't know that).
Useful resources
Or clone locally
git clone -b challenge/2026-03-27 https://github.com/niltonheck/typedrop.git