Tipos literales y uniones discriminadas

Intermedio
TypeScript
TypeScript
Actualizado: 18/04/2026

Tipos literales de string, number y boolean

Un tipo literal restringe una variable a un valor específico en lugar de aceptar cualquier valor de su tipo base. Mientras que string admite cualquier cadena, el tipo literal "norte" solo admite exactamente esa cadena:

Tipos literales y discriminated unions

let dirección: "norte" | "sur" | "este" | "oeste"

dirección = "norte" // Valido
dirección = "sur"   // Valido
// dirección = "arriba" // Error: Type '"arriba"' is not assignable

Tipos literales de string

Los tipos literales de cadena son los más utilizados. Permiten definir conjuntos cerrados de opciones válidas:

type MetodoHTTP = "GET" | "POST" | "PUT" | "DELETE" | "PATCH"

function realizarPeticion(url: string, método: MetodoHTTP): string {
  return `${método} ${url}`
}

realizarPeticion("/api/usuarios", "GET")    // Valido
realizarPeticion("/api/usuarios", "POST")   // Valido
// realizarPeticion("/api/usuarios", "TRACE") // Error: no es un MetodoHTTP válido

Son especialmente útiles en configuraciones y opciones de componentes:

type Tema = "claro" | "oscuro" | "sistema"
type Tamano = "pequeño" | "mediano" | "grande"
type Variante = "primario" | "secundario" | "peligro" | "exito"

type OpcionesBoton = {
  texto: string
  variante: Variante
  tamano: Tamano
  deshabilitado: boolean
}

function crearBoton(opciones: OpcionesBoton): string {
  return `<button class="${opciones.variante} ${opciones.tamano}">${opciones.texto}</button>`
}

const boton = crearBoton({
  texto: "Guardar",
  variante: "primario",
  tamano: "mediano",
  deshabilitado: false
})

Tipos literales numéricos

Los tipos literales numéricos restringen los valores a números específicos:

type DiaSemana = 1 | 2 | 3 | 4 | 5 | 6 | 7
type Puerto = 80 | 443 | 3000 | 8080

function nombreDia(dia: DiaSemana): string {
  const nombres = ["Lunes", "Martes", "Miercoles", "Jueves", "Viernes", "Sabado", "Domingo"]
  return nombres[dia - 1]
}

console.log(nombreDia(1)) // "Lunes"
console.log(nombreDia(5)) // "Viernes"
// nombreDia(8) // Error: Argument of type '8' is not assignable to parameter of type 'DiaSemana'

También sirven para representar códigos de estado o niveles fijos:

type CodigoHTTP = 200 | 201 | 204 | 400 | 401 | 403 | 404 | 500
type NivelLog = 0 | 1 | 2 | 3

function interpretarCodigo(código: CodigoHTTP): string {
  if (código >= 200 && código < 300) return "Exito"
  if (código >= 400 && código < 500) return "Error del cliente"
  return "Error del servidor"
}

Tipos literales booleanos

Los tipos literales booleanos true y false permiten restringir una propiedad a un valor booleano exacto:

type RespuestaExitosa<T> = {
  exito: true
  datos: T
}

type RespuestaFallida = {
  exito: false
  error: string
  código: number
}

type Respuesta<T> = RespuestaExitosa<T> | RespuestaFallida

function procesarRespuesta(respuesta: Respuesta<string[]>): string {
  if (respuesta.exito === true) {
    return `Datos: ${respuesta.datos.join(", ")}`
  }
  return `Error ${respuesta.código}: ${respuesta.error}`
}

Inferencia de literales con const y as const

Cuando se declara una variable con const, TypeScript infiere el tipo literal exacto. Con let, infiere el tipo amplio:

const modo = "producción"    // Tipo: "producción"
let modoVariable = "producción" // Tipo: string

La aserción as const permite obtener tipos literales en contextos donde TypeScript inferiría tipos amplios:

const config = {
  host: "localhost",
  puerto: 3000,
  modo: "desarrollo"
} as const
// Tipo: { readonly host: "localhost"; readonly puerto: 3000; readonly modo: "desarrollo" }

// Sin as const, el tipo sería { host: string; puerto: number; modo: string }

Esto es especialmente útil con arrays, que se convierten en tuplas de solo lectura:

const colores = ["rojo", "verde", "azul"] as const
// Tipo: readonly ["rojo", "verde", "azul"]

type Color = typeof colores[number]
// Tipo: "rojo" | "verde" | "azul"

function esColorValido(valor: string): valor is Color {
  return (colores as readonly string[]).includes(valor)
}

Uniones discriminadas

Las uniones discriminadas (también llamadas tagged unions) son un patrón que combina tipos de unión con un campo discriminante común. Este campo tiene un tipo literal diferente en cada variante, lo que permite a TypeScript identificar exactamente qué variante se está usando:

type Circulo = {
  forma: "circulo"
  radio: number
}

type Rectangulo = {
  forma: "rectangulo"
  ancho: number
  alto: number
}

type Triangulo = {
  forma: "triangulo"
  base: number
  altura: number
}

type Figura = Circulo | Rectangulo | Triangulo

El campo forma actúa como discriminante. Cada variante tiene un valor literal distinto para este campo, y TypeScript lo usa para estrechar el tipo:

function calcularArea(figura: Figura): number {
  switch (figura.forma) {
    case "circulo":
      // TypeScript sabe que figura es Circulo
      return Math.PI * figura.radio ** 2
    case "rectangulo":
      // TypeScript sabe que figura es Rectangulo
      return figura.ancho * figura.alto
    case "triangulo":
      // TypeScript sabe que figura es Triangulo
      return (figura.base * figura.altura) / 2
  }
}

console.log(calcularArea({ forma: "circulo", radio: 5 })) // 78.54
console.log(calcularArea({ forma: "rectangulo", ancho: 4, alto: 6 })) // 24

Gestión de estados con uniones discriminadas

Las uniones discriminadas son ideales para modelar estados de una aplicación:

type EstadoCarga = {
  estado: "cargando"
}

type EstadoExito<T> = {
  estado: "exito"
  datos: T
}

type EstadoError = {
  estado: "error"
  mensaje: string
  reintentable: boolean
}

type EstadoPeticion<T> = EstadoCarga | EstadoExito<T> | EstadoError

function renderizar(estado: EstadoPeticion<string[]>): string {
  switch (estado.estado) {
    case "cargando":
      return "Cargando..."
    case "exito":
      return `Elementos: ${estado.datos.join(", ")}`
    case "error":
      const reintento = estado.reintentable ? " (puede reintentar)" : ""
      return `Error: ${estado.mensaje}${reintento}`
  }
}

const cargando: EstadoPeticion<string[]> = { estado: "cargando" }
const exito: EstadoPeticion<string[]> = { estado: "exito", datos: ["a", "b"] }
const error: EstadoPeticion<string[]> = { estado: "error", mensaje: "Timeout", reintentable: true }

console.log(renderizar(cargando)) // "Cargando..."
console.log(renderizar(exito))    // "Elementos: a, b"
console.log(renderizar(error))    // "Error: Timeout (puede reintentar)"

Acciones en reducers

Las uniones discriminadas encajan perfectamente en patrones de tipo reducer:

type Tarea = {
  id: number
  texto: string
  completada: boolean
}

type AccionAgregar = {
  tipo: "AGREGAR"
  texto: string
}

type AccionCompletar = {
  tipo: "COMPLETAR"
  id: number
}

type AccionEliminar = {
  tipo: "ELIMINAR"
  id: number
}

type AccionEditar = {
  tipo: "EDITAR"
  id: number
  texto: string
}

type Accion = AccionAgregar | AccionCompletar | AccionEliminar | AccionEditar

function reducer(estado: Tarea[], accion: Accion): Tarea[] {
  switch (accion.tipo) {
    case "AGREGAR":
      return [...estado, { id: Date.now(), texto: accion.texto, completada: false }]
    case "COMPLETAR":
      return estado.map(t => t.id === accion.id ? { ...t, completada: !t.completada } : t)
    case "ELIMINAR":
      return estado.filter(t => t.id !== accion.id)
    case "EDITAR":
      return estado.map(t => t.id === accion.id ? { ...t, texto: accion.texto } : t)
  }
}

Pattern matching exhaustivo

El pattern matching exhaustivo garantiza que todos los casos posibles de una unión discriminada están cubiertos. TypeScript lo comprueba mediante el tipo never: si después de manejar todos los casos el tipo residual no es never, hay un caso sin cubrir:

type Operación =
  | { tipo: "sumar"; a: number; b: number }
  | { tipo: "restar"; a: number; b: number }
  | { tipo: "multiplicar"; a: number; b: number }
  | { tipo: "dividir"; a: number; b: number }

function calcular(op: Operación): number {
  switch (op.tipo) {
    case "sumar":
      return op.a + op.b
    case "restar":
      return op.a - op.b
    case "multiplicar":
      return op.a * op.b
    case "dividir":
      if (op.b === 0) throw new Error("Division por cero")
      return op.a / op.b
    default: {
      const _exhaustivo: never = op
      throw new Error(`Operación no soportada: ${_exhaustivo}`)
    }
  }
}

Si se añade una nueva variante a Operación sin actualizar el switch, TypeScript emitirá un error en el default porque el tipo residual ya no es never.

Se puede extraer la comprobación exhaustiva a una función auxiliar reutilizable:

function exhaustivo(valor: never): never {
  throw new Error(`Caso no contemplado: ${JSON.stringify(valor)}`)
}

type Permiso = "leer" | "escribir" | "administrar"

function describirPermiso(permiso: Permiso): string {
  switch (permiso) {
    case "leer":
      return "Solo lectura"
    case "escribir":
      return "Lectura y escritura"
    case "administrar":
      return "Acceso total"
    default:
      return exhaustivo(permiso)
  }
}

Exhaustividad con if-else

El pattern matching exhaustivo no se limita a switch. También funciona con cadenas de if-else:

type Moneda = "EUR" | "USD" | "GBP"

function simbolo(moneda: Moneda): string {
  if (moneda === "EUR") return "e"
  if (moneda === "USD") return "$"
  if (moneda === "GBP") return "lb"
  
  const _check: never = moneda
  throw new Error(`Moneda desconocida: ${_check}`)
}

Ejemplo completo: sistema de eventos

Un ejemplo que combina uniones discriminadas, tipos literales y pattern matching exhaustivo:

type EventoUsuario =
  | { tipo: "registro"; email: string; nombre: string }
  | { tipo: "login"; email: string; ip: string }
  | { tipo: "logout"; sesionId: string }
  | { tipo: "cambio_password"; email: string; exitoso: boolean }

type EventoSistema =
  | { tipo: "inicio"; versión: string }
  | { tipo: "error"; código: number; mensaje: string }
  | { tipo: "mantenimiento"; duracionMinutos: number }

type Evento = EventoUsuario | EventoSistema

function formatearEvento(evento: Evento): string {
  switch (evento.tipo) {
    case "registro":
      return `Nuevo usuario: ${evento.nombre} (${evento.email})`
    case "login":
      return `Login: ${evento.email} desde ${evento.ip}`
    case "logout":
      return `Logout: sesion ${evento.sesionId}`
    case "cambio_password":
      const estado = evento.exitoso ? "exitoso" : "fallido"
      return `Cambio password ${estado}: ${evento.email}`
    case "inicio":
      return `Sistema iniciado v${evento.versión}`
    case "error":
      return `Error ${evento.código}: ${evento.mensaje}`
    case "mantenimiento":
      return `Mantenimiento programado: ${evento.duracionMinutos} minutos`
    default:
      return exhaustivo(evento)
  }
}

function exhaustivo(valor: never): never {
  throw new Error(`Caso no contemplado: ${JSON.stringify(valor)}`)
}

const eventos: Evento[] = [
  { tipo: "inicio", versión: "2.0" },
  { tipo: "registro", email: "ana@ejemplo.com", nombre: "Ana" },
  { tipo: "login", email: "ana@ejemplo.com", ip: "192.168.1.1" },
  { tipo: "error", código: 500, mensaje: "Base de datos no disponible" }
]

for (const evento of eventos) {
  console.log(formatearEvento(evento))
}

Fuentes y referencias

Documentación oficial y recursos externos para profundizar en TypeScript

Documentación oficial de TypeScript
Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, TypeScript es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de TypeScript

Explora más contenido relacionado con TypeScript y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Comprender los tipos literales de string, number y boolean en TypeScript. Crear uniones de literales para representar conjuntos finitos de valores. Dominar las uniones discriminadas con campos discriminantes comunes. Aplicar pattern matching exhaustivo con switch y never. Usar as const para inferir tipos literales automáticamente.