12k
All articles

Guía Práctica del Operador `satisfies` de TypeScript

Explicación del operador satisfies de TypeScript con ejemplos de configuración, inferencia estrecha y cuándo usarlo frente a as o una anotación de tipo.

OpenReplay Team
OpenReplay Team
Guía Práctica del Operador `satisfies` de TypeScript

El operador satisfies valida un valor contra un tipo sin modificar el tipo inferido del valor, lo que permite obtener la seguridad del tipo y la precisión del valor al mismo tiempo. Este comportamiento resuelve un problema concreto y cotidiano: una anotación con dos puntos en un objeto de configuración protege contra valores incorrectos, pero descarta las claves literales y los tipos precisos que se querían conservar. Esta guía ofrece el modelo mental, el ejemplo canónico y una regla de decisión para elegir entre satisfies, as o una anotación con dos puntos. Todos los fragmentos de código están escritos para TypeScript 4.9 en adelante.

Puntos Clave

  • satisfies valida un valor contra un tipo mientras preserva el tipo inferido preciso del valor, por lo que el autocompletado y el estrechamiento de literales se mantienen.
  • Con una anotación de dos puntos, el tipo declarado tiene prioridad y el valor se amplía para ajustarse a él; con satisfies, el valor tiene prioridad y el tipo solo se usa para validarlo.
  • as no verifica el valor — anula el verificador de tipos, razón por la cual const user = {} as User compila sin errores pero falla en tiempo de ejecución al leer user.name.
  • Combinar una anotación de dos puntos con satisfies (const x: T = {…} satisfies T) es redundante: los dos puntos tienen prioridad y anulan el estrechamiento que se buscaba obtener.
  • satisfies opera únicamente en tiempo de compilación y no genera ningún código JavaScript; utilice un validador en tiempo de ejecución como Zod cuando los datos provengan de una red o un archivo.

El problema que resuelve satisfies

Dos patrones llevan a los desarrolladores a utilizar satisfies. El primero es una anotación con dos puntos en un objeto con claves, que amplía el tipo y destruye el autocompletado. El ejemplo de routes de Matt Pocock lo ilustra: al anotar con Record<string, {}>, se puede leer cualquier clave, incluso una incorrecta, sin que se produzca ningún error.

const routes: Record<string, {}> = {
  "/": {},
  "/users": {},
  "/admin/users": {},
};
routes.awdkjanwdkjn; // Sin error — el tipo ahora es Record<string, {}>

El segundo patrón es una propiedad con tipo unión. En el ejemplo de freeCodeCamp, una propiedad tipada como unión entre un literal de cadena y un objeto no permite llamar a métodos de cadena sin una comprobación manual.

type Info = "John" | "Jack" | { id: number; age: number };
type Person = { myInfo: Info; myOtherInfo: Info };

const applicant: Person = { myInfo: "John", myOtherInfo: { id: 123, age: 22 } };
applicant.myInfo.toUpperCase();
// Property 'toUpperCase' does not exist on type 'Info'

Se termina escribiendo if (typeof applicant.myInfo === "string") antes de cada acceso. Ambos problemas comparten la misma causa raíz: la anotación con dos puntos reemplazó el tipo específico del valor por uno declarado más amplio.

¿Qué hace el operador satisfies?

satisfies valida que una expresión coincida con un tipo sin modificar el tipo que TypeScript infiere para ella. Fue introducido en TypeScript 4.9, publicado el 15 de noviembre de 2022, y su comportamiento es idéntico en la versión estable actual TypeScript 6.0 y en el release candidate de la versión 7.0. Dado que el port a Go de la versión 7.0 mantuvo la semántica de verificación de tipos estructuralmente idéntica a la 6.0, satisfies aplica exactamente las mismas reglas en el nuevo compilador.

El modelo mental, en palabras de Pocock: con una anotación de dos puntos, el tipo prevalece sobre el valor; con satisfies, el valor prevalece sobre el tipo. Al usar satisfies, TypeScript infiere el tipo más preciso posible y utiliza la anotación únicamente para validarlo. Opera exclusivamente en tiempo de compilación — no genera ningún código JavaScript ni tiene costo en tiempo de ejecución, por lo que puede detectar errores tipográficos o tipos de valor incorrectos antes de que el código se ejecute.

El ejemplo canónico de satisfies

La solución consiste en mover la anotación de los dos puntos a un satisfies al final: ambos problemas desaparecen de inmediato — se conservan los tipos literales precisos y se sigue obteniendo un error ante un valor incorrecto.

const routes = {
  "/": {},
  "/users": {},
  "/admin/users": {},
} satisfies Record<string, {}>;

routes.awdkjanwdkjn;
// Property 'awdkjanwdkjn' does not exist on type
// '{ "/": {}; "/users": {}; "/admin/users": {}; }'

El autocompletado sobre routes ahora lista las rutas reales. La validación se mantiene: si se asigna algo que la anotación prohíbe, el compilador lo rechaza.

const routes = {
  "/": null, // Type 'null' is not assignable to type '{}'
} satisfies Record<string, {}>;

El mismo mecanismo resuelve el caso de la unión. applicant.myInfo se estrecha al literal "John", por lo que .toUpperCase() es válido sin ninguna comprobación adicional:

const applicant = {
  myInfo: "John",
  myOtherInfo: { id: 123, age: 22 },
} satisfies Person;

applicant.myInfo.toUpperCase(); // OK — inferido como "John"

satisfies vs as vs anotación con dos puntos

as no verifica el valor — anula el verificador de tipos, razón por la cual const user = {} as User compila sin errores y luego falla en tiempo de ejecución en el momento en que se lee user.name. Esa es la distinción fundamental entre las tres herramientas:

Herramienta¿Verifica el valor?¿Conserva la inferencia precisa?¿Permite engañar a TS?Usar cuando
: Type (dos puntos)No — amplía al tipoNoSe desea deliberadamente un tipo más amplio
satisfies TypeNoSe desea validación e inferencia precisa
as TypeNoN/ACasi nunca como opción predeterminada

El riesgo en tiempo de ejecución es concreto:

type User = { id: string; name: { first: string; last: string } };
const user = {} as User;
user.name.first; // Sin error en el IDE — falla en tiempo de ejecución

as también introduce errores silenciosos con el tiempo. Esto compila hoy, pero si se agrega un campo obligatorio a User, defaultUser se vuelve inválido sin ningún error:

type User = { id: string; name: string };
const defaultUser = { id: "123", name: "Matt" } as User;

Esta es precisamente la clase de error que el session replay está diseñado para detectar: se observa la forma real del objeto en el momento en que falló el acceso a una propiedad, no la forma que se afirmó. Al reemplazar as por satisfies, el compilador señala inmediatamente el campo faltante.

La regla general es: nunca recurrir a as por defecto — usar satisfies para validar mientras se conserva la inferencia, y usar una anotación con dos puntos solo cuando se desea deliberadamente un tipo más amplio que se reasignará más adelante.

La trampa de la anotación redundante

Combinar una anotación de dos puntos con satisfiesconst joe: TUser = {…} satisfies TUser — es redundante: la anotación de dos puntos tiene prioridad y anula silenciosamente el estrechamiento que satisfies pretendía preservar. El artículo de Refine.dev muestra las consecuencias — el acceso a una propiedad anidada falla porque el tipo declarado prevaleció y el estrechamiento interno fue descartado. Hay que elegir una sola opción. Si se desea el estrechamiento, se deben eliminar los dos puntos.

Dónde satisfies demuestra su valor

Utilice satisfies para configuraciones tipadas, mapas con clave Record y valores de uniones discriminadas que se deseen mantener con tipos precisos. Los mapas de temas y paletas son el caso arquetípico — el ejemplo oficial de palette valida cada entrada RGB mientras conserva los tipos literales por clave. Para claves opcionales, envuelva el record en Partial para que las claves ausentes estén permitidas, pero las presentes sigan siendo verificadas:

type Keys = "id" | "name" | "email" | "age";

const person = {
  id: 12345,
  name: "Jacky",
  email: "jacky@test.com",
} satisfies Partial<Record<Keys, string | number>>;

person.name.toUpperCase(); // estrechado a string

Cuándo no usar satisfies

Omita satisfies para un objeto simple donde una anotación : Type ya expresa todo lo necesario. Omítalo también cuando se desee el tipo más amplio — si se planea reasignar una variable más adelante, satisfies lo impedirá porque fija el tipo inferido preciso:

// Anotación con dos puntos — la reasignación es válida
let id: string | number = "123";
id = 456; // OK

// satisfies — el valor prevalece, por lo que se estrecha a string
let id2 = "123" satisfies string | number;
id2 = 456; // Type 'number' is not assignable to type 'string'

Y omítalo por completo para datos que no se controlan. satisfies nunca se ejecuta, por lo que no puede validar un payload JSON ni el envío de un formulario en tiempo de ejecución — utilice un validador de esquemas en tiempo de ejecución como Zod o io-ts cuando los datos atraviesen un límite de red o de archivo.

Recurra a satisfies siempre que esté tipando un literal que también desee mantener específico — configuraciones, mapas de rutas, paletas, valores de unión. Reemplace el uso reflejo de as por él, conserve las anotaciones con dos puntos para los casos en que un tipo más amplio sea el objetivo, y su próximo objeto de configuración mantendrá tanto su seguridad como su autocompletado.

Preguntas Frecuentes

¿Funciona el operador satisfies en archivos JavaScript con JSDoc?

Sí. TypeScript 5.0 incorporó una etiqueta JSDoc @satisfies que hace exactamente lo mismo que el operador satisfies en archivos TypeScript. En un archivo JavaScript, la etiqueta se escribe sobre una declaración — por ejemplo, una anotación @satisfies que nombra un tipo — y el verificador valida el valor contra ese tipo mientras conserva el tipo inferido preciso. Esto permite que los proyectos JavaScript tipados con JSDoc obtengan el mismo beneficio de validación con estrechamiento sin necesidad de migrar a archivos .ts.

¿Agrega satisfies algún costo en tiempo de ejecución o genera JavaScript adicional?

No. satisfies es un operador exclusivamente de tiempo de compilación, a nivel de tipos, que no emite ningún código JavaScript, por lo que no tiene costo en tiempo de ejecución ni impacto en el tamaño del bundle. La palabra clave y el tipo que la sigue se eliminan durante la compilación, exactamente igual que una anotación con dos puntos. Dado que no se ejecuta nada, tampoco puede validar datos en tiempo de ejecución, razón por la cual los payloads de red o las entradas de formularios siguen requiriendo un validador de esquemas en tiempo de ejecución como Zod.

¿Por qué mi objeto sigue fallando la verificación de tipos cuando uso tanto una anotación de dos puntos como satisfies?

Porque la anotación de dos puntos siempre tiene prioridad y la cláusula satisfies se convierte en sintaxis inerte. Escribir const config: Theme = {…} satisfies Theme significa que el tipo Theme declarado prevalece, el valor se amplía a Theme y el estrechamiento preciso que satisfies pretendía preservar se descarta. Acceder a una propiedad literal anidada falla entonces como si satisfies no estuviera presente. Elimine los dos puntos y conserve únicamente el satisfies al final para mantener el estrechamiento.

¿Cuándo debería usar Zod en lugar de satisfies para validar datos?

Use Zod, u otro validador de esquemas en tiempo de ejecución como io-ts, siempre que los datos lleguen en tiempo de ejecución desde una fuente que no controla, como una respuesta de red, un archivo JSON o el envío de un formulario. satisfies solo verifica literales escritos en el código fuente en tiempo de compilación y no emite código en tiempo de ejecución, por lo que no puede inspeccionar datos externos desconocidos. Use satisfies para configuraciones tipadas, mapas Record y valores de unión en su propio código; use Zod para los límites con el exterior.

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.