Unknown, never y tipos especiales

Avanzado
TypeScript
TypeScript
Actualizado: 18/04/2026

El tipo unknown

El tipo unknown representa un valor cuyo tipo es desconocido. A diferencia de any, que desactiva las comprobaciones de tipo, unknown obliga a verificar el tipo antes de realizar cualquier operación. Es la forma segura de manejar valores de tipo desconocido:

Jerarquia de tipos especiales: any, unknown, never

let valor: unknown = "hola"
valor = 42
valor = { nombre: "Ana" }
valor = [1, 2, 3]

// Todas las asignaciones son válidas, pero no podemos operar directamente
// valor.toUpperCase() // Error: 'valor' is of type 'unknown'
// valor.nombre // Error: 'valor' is of type 'unknown'
// valor + 1 // Error: 'valor' is of type 'unknown'

Para usar un valor unknown, primero hay que estrechar su tipo mediante comprobaciones:

function procesar(valor: unknown): string {
  if (typeof valor === "string") {
    return valor.toUpperCase()
  }
  if (typeof valor === "number") {
    return valor.toFixed(2)
  }
  if (Array.isArray(valor)) {
    return valor.join(", ")
  }
  return String(valor)
}

console.log(procesar("hola"))    // "HOLA"
console.log(procesar(3.14159))   // "3.14"
console.log(procesar([1, 2, 3])) // "1, 2, 3"

unknown vs any

La diferencia fundamental es que any elimina las comprobaciones de tipo, mientras que unknown las exige:

// Con any: sin seguridad
function peligroso(valor: any): string {
  return valor.nombre.toUpperCase() // Compila, pero puede fallar en runtime
}

// Con unknown: seguro
function seguro(valor: unknown): string {
  if (
    typeof valor === "object" &&
    valor !== null &&
    "nombre" in valor &&
    typeof (valor as { nombre: unknown }).nombre === "string"
  ) {
    return (valor as { nombre: string }).nombre.toUpperCase()
  }
  return "Sin nombre"
}

console.log(seguro({ nombre: "Ana" })) // "ANA"
console.log(seguro(42))                // "Sin nombre"
console.log(seguro(null))              // "Sin nombre"

Usa unknown en lugar de any cuando recibas datos de fuentes externas. El esfuerzo adicional de comprobar tipos previene errores en tiempo de ejecución.

Las reglas de asignabilidad son opuestas:

let valorAny: any = 10
let valorUnknown: unknown = 20

// any se puede asignar a cualquier tipo
let texto1: string = valorAny // Permitido (peligroso)

// unknown solo se puede asignar a unknown o any
// let texto2: string = valorUnknown // Error
let otro: unknown = valorUnknown // Permitido
let cualquiera: any = valorUnknown // Permitido

Patrones con unknown para datos externos

Al recibir datos de una API, parsear JSON o leer entrada del usuario, unknown es el tipo apropiado:

function parsearJSON(texto: string): unknown {
  return JSON.parse(texto)
}

interface Producto {
  id: number
  nombre: string
  precio: number
}

function esProducto(datos: unknown): datos is Producto {
  if (typeof datos !== "object" || datos === null) return false
  const obj = datos as Record<string, unknown>
  return (
    typeof obj.id === "number" &&
    typeof obj.nombre === "string" &&
    typeof obj.precio === "number"
  )
}

const json = '{"id": 1, "nombre": "Laptop", "precio": 999}'
const datos = parsearJSON(json)

if (esProducto(datos)) {
  console.log(`${datos.nombre}: ${datos.precio}e`) // "Laptop: 999e"
}

Un patrón más elaborado para respuestas de API con manejo de errores:

type RespuestaAPI<T> =
  | { ok: true; datos: T }
  | { ok: false; error: string }

function procesarRespuesta<T>(
  respuesta: unknown,
  validar: (datos: unknown) => datos is T
): RespuestaAPI<T> {
  if (typeof respuesta !== "object" || respuesta === null) {
    return { ok: false, error: "Respuesta inválida" }
  }

  const obj = respuesta as Record<string, unknown>

  if (obj.error) {
    return { ok: false, error: String(obj.error) }
  }

  if (validar(obj.datos)) {
    return { ok: true, datos: obj.datos }
  }

  return { ok: false, error: "Datos no válidos" }
}

interface Usuario {
  id: number
  nombre: string
  email: string
}

function esUsuario(datos: unknown): datos is Usuario {
  if (typeof datos !== "object" || datos === null) return false
  const obj = datos as Record<string, unknown>
  return (
    typeof obj.id === "number" &&
    typeof obj.nombre === "string" &&
    typeof obj.email === "string"
  )
}

const respuestaOk = { datos: { id: 1, nombre: "Ana", email: "ana@ej.com" } }
const resultado = procesarRespuesta(respuestaOk, esUsuario)

if (resultado.ok) {
  console.log(resultado.datos.nombre) // "Ana"
} else {
  console.log(resultado.error)
}

Operaciones seguras con unknown

Ciertas operaciones son válidas con unknown sin necesidad de narrowing:

let valor: unknown = 42

// Comparaciones de igualdad: siempre válidas
console.log(valor === 42)    // true
console.log(valor !== "hola") // true

// typeof: siempre válido
console.log(typeof valor) // "number"

// Negacion lógica: siempre válida
console.log(!valor) // false

// Operadores logicos con unknown
const resultado = valor || "default" // tipo: unknown
const definido = valor ?? "default"  // tipo: unknown

El tipo never

El tipo never representa valores que nunca pueden existir. Es el tipo más restrictivo de TypeScript: ningún valor es asignable a never (excepto never mismo). Se utiliza en tres contextos principales:

Funciones que nunca retornan

Una función que siempre lanza un error o contiene un bucle infinito tiene tipo de retorno never:

function lanzarError(mensaje: string): never {
  throw new Error(mensaje)
}

function fallarConCodigo(código: number, mensaje: string): never {
  console.error(`[${código}] ${mensaje}`)
  throw new Error(`Error ${código}: ${mensaje}`)
}

function procesarInfinito(): never {
  while (true) {
    // Proceso que nunca termina
  }
}

A diferencia de void, que indica que la función retorna sin valor, never indica que la función nunca llega a retornar:

function logear(mensaje: string): void {
  console.log(mensaje)
  // Retorna implícitamente undefined
}

function fallar(mensaje: string): never {
  throw new Error(mensaje)
  // Nunca se llega al punto de retorno
}

Never en el análisis de control de flujo

Cuando TypeScript agota todas las posibilidades de una unión, el tipo residual es never:

function ejemplo(valor: string | number): string {
  if (typeof valor === "string") {
    return valor.toUpperCase()
  }
  if (typeof valor === "number") {
    return valor.toFixed(2)
  }
  // TypeScript infiere que valor es never aquí
  // porque string y number ya están cubiertos
  const imposible: never = valor
  return imposible
}

Never en switch exhaustivos

El uso más práctico de never es garantizar que un switch cubre todos los casos de una unión discriminada:

type EstadoPedido = "pendiente" | "procesando" | "enviado" | "entregado" | "cancelado"

function mensajeEstado(estado: EstadoPedido): string {
  switch (estado) {
    case "pendiente":
      return "Su pedido está pendiente de confirmacion"
    case "procesando":
      return "Su pedido se está preparando"
    case "enviado":
      return "Su pedido está en camino"
    case "entregado":
      return "Su pedido ha sido entregado"
    case "cancelado":
      return "Su pedido fue cancelado"
    default: {
      const _exhaustivo: never = estado
      throw new Error(`Estado no contemplado: ${_exhaustivo}`)
    }
  }
}

Si alguien agrega un nuevo estado a EstadoPedido sin actualizar el switch, TypeScript genera un error de compilación porque el nuevo estado no es asignable a never.

Se puede crear una función auxiliar reutilizable:

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

type Formato = "json" | "xml" | "csv"

function serializar(datos: Record<string, string>, formato: Formato): string {
  switch (formato) {
    case "json":
      return JSON.stringify(datos)
    case "xml":
      return Object.entries(datos).map(([k, v]) => `<${k}>${v}</${k}>`).join("")
    case "csv":
      return Object.entries(datos).map(([k, v]) => `${k},${v}`).join("\n")
    default:
      return exhaustivo(formato)
  }
}

Never en tipos condicionales

En tipos condicionales, never actúa como filtro porque desaparece de las uniones:

type SoloStrings<T> = T extends string ? T : never
type SoloNumeros<T> = T extends number ? T : never

type Mezcla = "hola" | 42 | true | "mundo" | 100

type Textos = SoloStrings<Mezcla>  // "hola" | "mundo"
type Números = SoloNumeros<Mezcla> // 42 | 100

Este patrón es la base de utility types como Extract y Exclude:

// Extract<T, U> es equivalente a: T extends U ? T : never
type Resultado = Extract<string | number | boolean, string | number>
// Tipo: string | number

// Exclude<T, U> es equivalente a: T extends U ? never : T
type SinString = Exclude<string | number | boolean, string>
// Tipo: number | boolean

Diferencias entre void, never y undefined

Estos tres tipos representan conceptos diferentes y se usan en contextos distintos:

// void: la función retorna pero sin valor útil
function saludar(nombre: string): void {
  console.log(`Hola, ${nombre}`)
}

// undefined: la función retorna explícitamente undefined
function buscar(id: number): string | undefined {
  if (id === 1) return "Ana"
  return undefined
}

// never: la función nunca retorna
function fallar(msg: string): never {
  throw new Error(msg)
}

Las diferencias de asignabilidad:

// void acepta undefined
let v: void = undefined

// undefined es un valor concreto
let u: undefined = undefined

// never no acepta ningun valor
// let n: never = undefined // Error
// let n: never = null // Error

// void y undefined pueden usarse como tipos de retorno
function f1(): void { }
function f2(): undefined { return undefined }
// function f3(): never { } // Error: no puede retornar

void en callbacks

El tipo void tiene un comportamiento especial en callbacks: permite que la función callback devuelva un valor que será ignorado:

type Callback = (item: string) => void

const items = ["a", "b", "c"]

// Array.push devuelve number, pero la firma espera void
// Esto es válido porque void en callbacks permite ignorar el retorno
const cb: Callback = (item) => items.push(item)

Esto es deliberado en TypeScript para mantener compatibilidad con patrones comunes de JavaScript donde se pasan funciones que retornan valores a contextos que no los usan.

Patrones prácticos combinando los tres

type Resultado<T> =
  | { estado: "exito"; valor: T }
  | { estado: "vacío" }
  | { estado: "error"; mensaje: string }

function procesarResultado<T>(resultado: Resultado<T>): T {
  switch (resultado.estado) {
    case "exito":
      return resultado.valor
    case "vacío":
      throw new Error("Resultado vacío") // never implícitamente
    case "error":
      throw new Error(resultado.mensaje) // never implícitamente
    default: {
      const _: never = resultado
      throw new Error("Estado imposible")
    }
  }
}

// Función que usa void para logging
function conLog<T>(
  operación: () => T,
  logger: (mensaje: string) => void
): T {
  logger("Iniciando operación")
  const resultado = operación()
  logger("Operación completada")
  return resultado
}

// Función que usa undefined para busquedas
function buscarEnLista<T>(
  items: T[],
  predicado: (item: T) => boolean
): T | undefined {
  for (const item of items) {
    if (predicado(item)) return item
  }
  return undefined
}

interface Tarea {
  id: number
  titulo: string
  completada: boolean
}

const tareas: Tarea[] = [
  { id: 1, titulo: "Estudiar TypeScript", completada: false },
  { id: 2, titulo: "Escribir tests", completada: true }
]

const tarea = buscarEnLista(tareas, t => t.id === 1)
if (tarea !== undefined) {
  console.log(tarea.titulo) // TypeScript sabe que tarea es Tarea
}

Un ejemplo completo que muestra el uso coordinado de unknown, never, void y undefined:

type Validador<T> = {
  validar: (datos: unknown) => datos is T
  mensajeError: string
}

function crearValidador<T>(
  validar: (datos: unknown) => datos is T,
  mensajeError: string
): Validador<T> {
  return { validar, mensajeError }
}

function parsearYValidar<T>(
  json: string,
  validador: Validador<T>,
  onError: (error: string) => void
): T | undefined {
  let datos: unknown
  
  try {
    datos = JSON.parse(json)
  } catch {
    onError("JSON inválido")
    return undefined
  }
  
  if (validador.validar(datos)) {
    return datos
  }
  
  onError(validador.mensajeError)
  return undefined
}

interface Config {
  host: string
  puerto: number
}

const validadorConfig = crearValidador<Config>(
  (datos: unknown): datos is Config => {
    if (typeof datos !== "object" || datos === null) return false
    const obj = datos as Record<string, unknown>
    return typeof obj.host === "string" && typeof obj.puerto === "number"
  },
  "Configuración inválida: se requiere host (string) y puerto (number)"
)

const errores: string[] = []
const config = parsearYValidar(
  '{"host": "localhost", "puerto": 3000}',
  validadorConfig,
  (err) => errores.push(err)
)

if (config !== undefined) {
  console.log(`Servidor en ${config.host}:${config.puerto}`)
} else {
  console.log(`Errores: ${errores.join(", ")}`)
}

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 el tipo unknown como alternativa segura a any. Dominar el tipo never para valores imposibles y comprobaciones exhaustivas. Diferenciar entre void, never y undefined. Aplicar patrones practicos con unknown para respuestas de API y parseo de JSON. Usar never en switch exhaustivos para garantizar cobertura completa de casos.