any, unknown y never en TypeScript: cuál usar

1. Visión general

TypeScript tiene tres tipos especiales que causan confusión: any, unknown y never. Los tres aparecen en los “límites” del sistema de tipos. any desactiva la verificación de tipos, unknown te obliga a verificar antes de usar, mientras que never representa un caso que no puede ocurrir.

Después de leer este artículo, sabrás qué tipo elegir para datos de origen desconocido. También aprenderás cómo usar never para que el compilador te reporte errores si olvidas manejar una rama.

Todos los ejemplos corren con TypeScript 5.x y "strict": true en tsconfig.json.

2. any: desactivar type checker

any permite que un valor haga cualquier cosa. El compilador no verifica nada:

let payload: any = "hola";

payload.toUpperCase();   // OK
payload.foo.bar();       // OK en compile, error en runtime
payload = 42;
const total: number = payload * 2; // OK

La tercera línea se compila sin problemas. Cuando se ejecuta, lanza un error:

TypeError: Cannot read properties of undefined (reading 'bar')

Con any, TypeScript asume que tú eres responsable. Pierdes autocompletado, pierdes verificación de nombres de propiedades, y pierdes los beneficios del refactoring seguro.

2.1. any se propaga

El mayor problema de any es que se propaga a todo lo que toca:

function loadConfig(): any {
  return JSON.parse('{"port": "8080"}');
}

const config = loadConfig();
const port = config.port;      // port: any
const next = port + 1;         // next: any
console.log(next);
80801

port es la cadena "8080", entonces port + 1 da como resultado "80801" en lugar de 8081. El compilador no advierte, porque any se propagó a través de config, port y luego next. Un solo any al principio puede hacer perder la verificación de tipos en toda una cadena de llamadas de función.

Nota: JSON.parse, response.json() en fetch API y muchas librerías antiguas devuelven any. Esta es la fuente más común de fugas de any en código real.

3. unknown: seguro cuando no conoces el tipo

unknown también acepta cualquier valor como any. La diferencia es que no puedes usar ese valor hasta que hayas reducido su tipo (narrowing):

let payload: unknown = "hola";

payload.toUpperCase();
error TS18046: 'payload' is of type 'unknown'.

El compilador te lo bloquea inmediatamente. Debes probar que payload es una cadena primero:

if (typeof payload === "string") {
  console.log(payload.toUpperCase()); // payload: string
}
HOLA

Dentro del bloque if, TypeScript sabe que payload es string, entonces toUpperCase() es válido.

3.1. Formas de reducir unknown

Hay cuatro herramientas principales:

class PaymentError extends Error {
  constructor(message: string, public readonly code: string) {
    super(message);
  }
}

function describe(value: unknown): string {
  if (typeof value === "number") {
    return `Número: ${value.toFixed(2)}`;
  }
  if (value instanceof PaymentError) {
    return `Error de pago ${value.code}`;
  }
  if (typeof value === "object" && value !== null && "id" in value) {
    return `Objeto con id: ${String(value.id)}`;
  }
  return "Desconocido";
}

console.log(describe(12.5));
console.log(describe(new PaymentError("Sin fondos", "INSUFFICIENT")));
console.log(describe({ id: 7 }));
console.log(describe(null));
Número: 12.50
Error de pago INSUFFICIENT
Objeto con id: 7
Desconocido
  • typeof se usa para tipos primitivos: string, number, boolean…
  • instanceof se usa para clases.
  • in comprueba si una propiedad existe. Desde TypeScript 4.9, después de "id" in value entonces value tiene el tipo object & Record<"id", unknown>.
  • Debes comprobar value !== null porque typeof null === "object".

3.2. Type guard para datos de fuentes externas

Para objetos complejos, debes escribir un type guard, es decir, una función que devuelve value is T:

interface Order {
  id: string;
  customerName: string;
  total: number;
}

export function isOrder(value: unknown): value is Order {
  if (typeof value !== "object" || value === null) return false;
  const v = value as Record<string, unknown>;
  return (
    typeof v.id === "string" &&
    typeof v.customerName === "string" &&
    typeof v.total === "number"
  );
}

export function parseOrder(json: string): Order {
  const data: unknown = JSON.parse(json);
  if (!isOrder(data)) {
    throw new Error("Datos de pedido inválidos");
  }
  return data; // data: Order
}

console.log(parseOrder('{"id":"A1","customerName":"María","total":250000}'));
{ id: 'A1', customerName: 'María', total: 250000 }

El punto importante es la línea const data: unknown = JSON.parse(json). Asignar explícitamente el tipo unknown bloquea que any de JSON.parse se propague hacia afuera. De aquí en adelante, el compilador te obliga a verificar antes de usar data.

En proyectos grandes, puedes reemplazar el type guard hecho a mano con una librería de validación de esquemas como Zod. El principio sigue siendo el mismo: datos que entran son unknown, después de validar se convierten al tipo específico.

3.3. unknown en catch

Con strict: true (específicamente el indicador useUnknownInCatchVariables), la variable en catch tiene tipo unknown:

try {
  parseOrder("{}");
} catch (err) {
  // err: unknown
  const message = err instanceof Error ? err.message : String(err);
  console.log(message);
}
Datos de pedido inválidos

Esto se alinea con la realidad. JavaScript permite throw en absolutamente cualquier cosa, incluso cadenas o números, así que no puedes asumir que err siempre es un Error.

4. never: el tipo sin valores

never es el tipo vacío. Ningún valor pertenece al tipo never. Aparece en tres situaciones principales.

4.1. Una función nunca retorna

Una función que siempre lanza un error, o corre en un bucle infinito, tiene el tipo de retorno never:

function fail(message: string): never {
  throw new Error(message);
}

function getDiscount(code: string): number {
  if (code === "TET2026") return 0.2;
  return fail(`Código de descuento inexistente: ${code}`);
}

Porque never se puede asignar a cualquier tipo, return fail(...) es válido en una función que retorna number. El compilador también entiende que el código después de la llamada a fail() es inalcanzable.

4.2. Verificación exhaustiva

Este es el uso más útil de never. Supongamos que tienes una unión de métodos de pago:

type PaymentMethod =
  | { kind: "card"; last4: string }
  | { kind: "cash" }
  | { kind: "momo"; phone: string };

export function assertNever(value: never): never {
  throw new Error(`Caso no manejado: ${JSON.stringify(value)}`);
}

export function paymentLabel(method: PaymentMethod): string {
  switch (method.kind) {
    case "card":
      return `Tarjeta **** ${method.last4}`;
    case "cash":
      return "Efectivo";
    case "momo":
      return `MoMo ${method.phone}`;
    default:
      return assertNever(method);
  }
}

Después de manejar los tres case, en la rama default entonces method se reduce a never. La llamada a assertNever(method) es por lo tanto válida.

Ahora, supongamos que tu compañero agrega { kind: "zalopay"; phone: string } a la unión pero olvida actualizar el switch. El compilador lo reporta inmediatamente:

error TS2345: Argument of type '{ kind: "zalopay"; phone: string; }' is not assignable to parameter of type 'never'.

El error se detecta en tiempo de compilación, en lugar de dejar que el usuario vea una etiqueta undefined en la pantalla de pago. Si datos extraños de todas formas se cuelan en tiempo de ejecución, por ejemplo desde una API antigua, assertNever lanzará un error claro.

4.3. never en conditional type

En tipos condicionales, never se usa para excluir un miembro de una unión, porque A | never es simplemente A:

type OnlyStrings<T> = T extends string ? T : never;

type Mixed = "vnd" | "usd" | 404 | null;
type Currencies = OnlyStrings<Mixed>; // "vnd" | "usd"

TypeScript distribuye tipos condicionales sobre cada miembro de la unión. Los miembros 404 y null se convierten a never y luego desaparecen. Los tipos de utilidad integrados como Exclude, Extract y NonNullable funcionan todos según este mecanismo.

5. Comparación rápida

La diferencia esencial radica en la relación de asignabilidad:

Asignar otro valor a este tipo Asignar este tipo a otro tipo Usar directamente (llamar método, acceder propiedad)
any Sí Sí (excepto never) Sí, sin verificación
unknown Sí Solo a unknown, any No, debe reducirse
never No (excepto never) Sí, a cualquier tipo Sin valor que usar

Se puede resumir así:

  • unknown es el top type seguro. Todo es unknown.
  • never es el bottom type. never es subtipo de todos los tipos.
  • any es la salida. Actúa como top type y bottom type a la vez, y por eso rompe la solidez del sistema de tipos.

6. Probar el comportamiento

Puedes verificar tanto el comportamiento en tiempo de ejecución como los tipos en tiempo de compilación con Vitest. Instala:

npm install -D vitest typescript

Supongamos que las funciones de la sección 3.2 y 4.2 están en order.ts y payment.ts:

// payment.test.ts
import { describe, it, expect, expectTypeOf } from "vitest";
import { isOrder, parseOrder } from "./order";
import { paymentLabel, assertNever } from "./payment";

describe("unknown y type guard", () => {
  it("acepta un pedido válido", () => {
    const order = parseOrder('{"id":"A1","customerName":"María","total":1000}');
    expect(order.total).toBe(1000);
    expectTypeOf(order.total).toEqualTypeOf<number>();
  });

  it("rechaza datos de tipo incorrecto", () => {
    expect(isOrder({ id: "A1", customerName: "María", total: "1000" })).toBe(false);
    expect(() => parseOrder("null")).toThrow("Datos de pedido inválidos");
  });
});

describe("never y verificación exhaustiva", () => {
  it("retorna la etiqueta correcta para cada método", () => {
    expect(paymentLabel({ kind: "card", last4: "4242" })).toBe("Tarjeta **** 4242");
    expect(paymentLabel({ kind: "cash" })).toBe("Efectivo");
  });

  it("lanza error cuando encuentra datos fuera de la unión en runtime", () => {
    const legacy = { kind: "paypal" } as unknown as never;
    expect(() => assertNever(legacy)).toThrow("Caso no manejado");
  });
});
npx vitest run
 ✓ payment.test.ts (4 tests)
 Test Files  1 passed (1)
      Tests  4 passed (4)

expectTypeOf verifica el tipo en tiempo de compilación. Si algún cambio hace que order.total se convierta en any, el test fallará cuando ejecutes type check (vitest --typecheck). La última prueba simula datos “extraños” de un sistema antiguo, para asegurarse de que la red de seguridad en tiempo de ejecución sigue funcionando.

7. Errores comunes y cuándo no usar estos tipos

Usar any para “pasar” errores de compilación. Este es el peor hábito. Si realmente no conoces el tipo, usa unknown. Activa la regla @typescript-eslint/no-explicit-any para atrapar estos casos.

Hacer casting de unknown usando as en lugar de reducir. data as Order solo tranquiliza al compilador sin verificar nada en tiempo de ejecución. Es fundamentalmente tan peligroso como any.

const data: unknown = JSON.parse('{"id": 1}');
const order = data as Order; // compile OK
order.customerName.trim();   // TypeError en runtime

Usar unknown para todo. unknown es para los límites del sistema: entrada del usuario, respuesta de API, catch, datos deserializados. Dentro de tu dominio, usa tipos específicos. Una función calculateTotal(items: unknown) es un mal diseño.

Confundir never con void. void significa que una función retorna pero no tiene un valor útil. never significa que una función nunca retorna. Una función log() normal debe ser void, no never.

Olvidar default en switch. Sin assertNever, TypeScript solo reporta un error si la función tiene un tipo de retorno explícito y noImplicitReturns está activado. Incluso entonces, pierdes la red de seguridad en tiempo de ejecución.

Nota: any todavía tiene usos legítimos. Es cuando migras gradualmente una gran base de código JavaScript, o cuando escribes declaraciones para una librería donde el tipo es realmente muy dinámico. Mantenlo en un área muy limitada y déjalo con un comentario explicando por qué.

8. Conclusión

Estos tres tipos tienen tres roles claros:

  • Usa unknown para datos de tipo desconocido en los límites del sistema, luego redúcelo usando typeof, instanceof, in o type guard.
  • Usa never para funciones que siempre lanzan un error, para verificación exhaustiva con assertNever, y para filtrar uniones en tipos condicionales.
  • Evita any, a menos que tengas una razón específica y lo hayas limitado.

La regla práctica es muy simple. Cuando estés a punto de escribir any, intenta unknown primero. Si el compilador reporta un error, esos son exactamente los lugares donde necesitas verificar los datos.

typescripttype-safetyunknownneverany