Module: TypeScript
TypeScript·156·4 MIN READ

156: TypeScript - Generics and Utility Types

TOPICS COVERED: 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:

ts
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

  1. What relationship does the generic preserve?
  2. Why is Partial<Task> usually too broad for an update command?
  3. 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.

References