156: TypeScript - Generics and Utility Types
Learning outcomes
- write useful generic functions with constraints;
- use utility types without hiding domain meaning;
- recognize when a generic abstraction is premature.
Study
Generics preserve relationships between inputs and outputs. A constraint says what operations are safe; it does not make every value valid. Pick, Omit, Partial, and Record are tools for specific boundaries, not substitutes for naming important domain types.
Practice
Build a typed paginate<T> helper and an API envelope type. Model CreateTaskInput, UpdateTaskInput, and TaskResponse separately. Test empty pages, invalid cursors, and a response containing an unexpected field.
Worked example
The generic preserves the item type while the cursor contract stays explicit:
type Page<T> = { items: T[]; nextCursor?: string }
function paginate<T>(items: readonly T[], size: number, cursor = 0): Page<T> {
if (!Number.isInteger(size) || size < 1) throw new RangeError('invalid page size')
const next = cursor + size
return { items: items.slice(cursor, next), ...(next < items.length ? { nextCursor: String(next) } : {}) }
}
The helper does not validate a cursor received from a client; the API layer must parse and bound it. Avoid turning every response into ApiResponse<T> if different failures need different recovery behavior.
Interview questions
- What relationship does the generic preserve?
- Why is
Partial<Task>usually too broad for an update command? - What runtime checks remain necessary after this helper typechecks?
Checkpoint
Explain why first<T>(items: T[]): T | undefined preserves information while first(items: unknown[]): unknown loses it. Identify one utility type that would make the task API less clear.
