← React & TypeScript

TypeScript Fundamentals

Which TypeScript utility types do you know and what are they for?

Show answer — try answering out loud first

They're generic types built into TypeScript that transform existing types instead of duplicating them: Partial makes everything optional, Pick and Omit select or remove properties, Record builds key-value objects, and ReturnType extracts a function's return type.

The core idea: define your base type once and derive the variants. If the base type changes tomorrow, all the variants update on their own.

The most used ones:

  • Partial<T>: all properties become optional. Perfect for update functions where you only send the fields that change. Required<T> is the opposite: all required.
  • Pick<T, K>: a new type with only the properties you choose.
  • Omit<T, K>: a new type without the properties you specify. Typical: remove id and creadoEn for the creation form.
  • Record<K, V>: an object whose keys are K and whose values are V. Very useful with unions of literals as keys.
  • ReturnType<F>: extracts the type a function returns, without writing it by hand.

The signal to use them: when you're about to copy and paste a type while changing a couple of things.

interface Usuario {
  id: number;
  nombre: string;
  email: string;
  rol: "admin" | "usuario";
}

// Partial: update only some fields
function actualizarUsuario(id: number, cambios: Partial<Usuario>): void {
  console.log(`Actualizando ${id}`, cambios);
}
actualizarUsuario(1, { nombre: "Ana María" }); // doesn't require the rest

// Pick: only what's needed for a profile card (nombre + email)
type TarjetaPerfil = Pick<Usuario, "nombre" | "email">;

// Omit: the sign-up form doesn't have an id yet
type NuevoUsuario = Omit<Usuario, "id">;
const alta: NuevoUsuario = { nombre: "Luis", email: "luis@mail.com", rol: "usuario" };

// Record: a typed dictionary of permissions by role
type Permisos = Record<Usuario["rol"], string[]>;
// If a role is missing or there's an extra key, it won't compile
const permisos: Permisos = { admin: ["leer", "borrar"], usuario: ["leer"] };

// ReturnType: reuse the type a function returns
function crearSesion(usuario: Usuario) {
  return { token: "abc123", usuario, expira: Date.now() + 3600 };
}
// Sesion = { token: string; usuario: Usuario; expira: number }
type Sesion = ReturnType<typeof crearSesion>;

Duplicating the type by hand instead of deriving it:

// (uses the Usuario interface from the previous example)
// Duplicate: if Usuario changes, this silently gets out of sync
interface UsuarioEditableMal {
  nombre?: string;
  email?: string;
  rol?: "admin" | "usuario";
}

// Derived: it updates on its own when Usuario changes
type UsuarioEditable = Partial<Omit<Usuario, "id">>;

The problem with the duplicate isn't writing it today, it's maintaining it tomorrow: someone adds a field to Usuario and the manual copy never finds out.

"Utility types are type transformers that TypeScript ships with so you don't duplicate definitions. The ones I use most: Partial for update functions where I only send the fields that change, Omit for variants like 'user without id' in creation forms, Pick when a component only needs two or three properties, Record for typed dictionaries, and ReturnType to extract what a function returns. The advantage is maintenance: I define the base type once and the variants update on their own when it changes."

Quick challenge

With the Usuario interface from the example: 1) define VistaPublica with only nombre and rol; 2) define BorradorUsuario where everything is optional except that it doesn't include id; 3) define UsuariosPorEmail, a dictionary from string to Usuario.

See solution
interface Usuario {
  id: number;
  nombre: string;
  email: string;
  rol: "admin" | "usuario";
}

// 1) Only two properties
type VistaPublica = Pick<Usuario, "nombre" | "rol">;

// 2) Without id and everything optional: utility types can be combined
type BorradorUsuario = Partial<Omit<Usuario, "id">>;

// 3) Dictionary with string key -> Usuario
type UsuariosPorEmail = Record<string, Usuario>;

const vista: VistaPublica = { nombre: "Ana", rol: "admin" };
const borrador: BorradorUsuario = { nombre: "Luis" };