learning-front

Nivel 5 · TypeScript: JavaScript con red de seguridad

as const y satisfies

Por qué TypeScript ensancha los tipos literales, cómo as const los congela y cómo satisfies valida una forma sin perder precisión.

En los capítulos anteriores TypeScript siempre te dejó escribir el tipo explícito de una variable. Pero muchas veces no lo escribes — dejas que TypeScript lo infiera — y entonces entra en juego una regla silenciosa: el widening. Entenderla es la base de este capítulo, porque as const y satisfies son la respuesta directa al problema que el widening introduce.

Widening: por qué TypeScript ensancha los tipos#

Cuando escribes let x = "Daño", TypeScript no pone tipo "Daño" a x. Pone string. ¿Por qué? Porque let permite reasignar: x = "Tanque" es válido, así que el tipo debe ser lo bastante amplio para admitirlo. TypeScript ensancha el tipo literal al tipo general (string, number, boolean) cuando ve que el valor podría cambiar.

Con const es distinto: el valor no puede reasignarse, así que TypeScript puede ser más preciso y conservar el literal:

typescript
// let: el valor puede cambiar → TypeScript infiere string (no "Daño")
let rolFlex = "Daño";

// const: el valor no puede cambiar → TypeScript infiere "Daño" (el literal)
const rolFijo = "Daño";

Hasta aquí parece que const soluciona el problema. Pero hay un caso donde el widening ocurre aunque uses const: las propiedades de un objeto literal. Las propiedades sí pueden reasignarse aunque la variable sea const, así que TypeScript las ensancha igualmente:

typescript
// const no congela las propiedades: solo impide reasignar la variable entera.
const heroe = { nombre: "Tracer", rol: "Daño" };

// heroe.rol tiene tipo string, no "Daño".
// Por eso esta asignación es válida:
heroe.rol = "Tanque";

¿Y qué? Si rol es string en vez de "Daño", TypeScript no puede comprobar que el rol que pones es uno de los roles válidos. Un "Soporte" colado en tiempo de ejecución no daría error de compilación. El tipo pierde su capacidad de protegerte.

as const: congela el tipo más estrecho#

as const es una aserción que le dice a TypeScript: “trata este valor como si fuera completamente inmutable, y dale el tipo más preciso posible”.

Sobre un array, as const hace dos cosas: convierte cada elemento a su tipo literal y marca el array como readonly, lo que impide mutaciones:

typescript
// Sin as const: string[] — se pierde qué literales hay dentro
const rolesSinCongelar = ["Daño", "Tanque", "Apoyo"];

// Con as const: readonly ["Daño", "Tanque", "Apoyo"]
// Cada posición es un literal; el array no se puede mutar.
const ROLES = ["Daño", "Tanque", "Apoyo"] as const;

Sobre un objeto, as const congela cada propiedad: pasa a ser readonly y su tipo es el literal exacto en vez del tipo general:

typescript
// Sin as const: { tamanoEquipo: number; rolPorDefecto: string }
const configSuelta = { tamanoEquipo: 5, rolPorDefecto: "Apoyo" };

// Con as const: { readonly tamanoEquipo: 5; readonly rolPorDefecto: "Apoyo" }
const CONFIG = { tamanoEquipo: 5, rolPorDefecto: "Apoyo" } as const;

Ahora CONFIG.rolPorDefecto tiene tipo "Apoyo", no string. TypeScript sabe exactamente qué valor hay ahí, y cualquier código que intente compararlo o asignarlo tiene toda la información.

Cuando veas typeof y keyof en el capítulo siguiente, podrás hacer algo más con un array as const: derivar una unión de los literales que contiene ("Daño" | "Tanque" | "Apoyo" a partir del propio array). Por ahora, quédate con que as const congela y deja los literales accesibles — eso es la base.

satisfies: valida la forma sin ensanchar#

as const resuelve el widening, pero tiene un límite: no valida que el objeto tenga la forma correcta. Si le pones una clave de más o usas un rol que no existe, TypeScript no dice nada.

Para eso está satisfies. Funciona así: TypeScript comprueba que el valor cumple la forma del tipo indicado, pero el tipo que asigna a la variable es el tipo concreto inferido, no el tipo general:

typescript
type Rol = "Daño" | "Tanque" | "Apoyo";

interface ConfigTeam {
  tamanoEquipo: number;
  modoJuego: string;
  rolPorDefecto: Rol;
}

// Anotación normal: TypeScript ensancha rolPorDefecto a Rol.
// configAnotada.rolPorDefecto tiene tipo Rol, no "Apoyo".
const configAnotada: ConfigTeam = {
  tamanoEquipo: 5,
  modoJuego: "Clasificatoria",
  rolPorDefecto: "Apoyo",
};

// satisfies: TypeScript verifica que encaja con ConfigTeam
// pero conserva el tipo concreto. rolPorDefecto sigue siendo "Apoyo".
const configSatisfies = {
  tamanoEquipo: 5,
  modoJuego: "Clasificatoria",
  rolPorDefecto: "Apoyo",
} satisfies ConfigTeam;

¿Y qué importa si rolPorDefecto es Rol o "Apoyo"? Importa cuando quieres que TypeScript compruebe valores concretos. Con tipo Rol, cualquier comparación o lógica que dependa de que sea exactamente "Apoyo" pierde precisión. Con el literal conservado, TypeScript puede razonarlo.

El error de satisfies aparece en la definición, no después. Si escribes rolPorDefecto: "Healer", falla ahí y no en algún punto de uso lejano. Eso hace los errores más fáciles de encontrar.

as const + satisfies: lo mejor de los dos#

Cada uno resuelve un problema distinto:

  • as consttipos estrechos y propiedades readonly. No valida la forma.
  • satisfies → valida la forma y conserva los literales. Las propiedades no son readonly.

Usados juntos, con la sintaxis as const satisfies T, obtienes los dos beneficios en una sola línea:

typescript
const CONFIG = {
  tamanoEquipo: 5,
  modoJuego: "Clasificatoria",
  rolPorDefecto: "Apoyo",
} as const satisfies ConfigTeam;

Ahora CONFIG.rolPorDefecto es "Apoyo" (no Rol), todas las propiedades son readonly, y si cambias cualquier valor a algo que no encaja con ConfigTeam, el error aparece aquí. Este es el patrón habitual para constantes de módulo en proyectos reales: un objeto de configuración que nunca debe mutar, validado en compilación y con tipos tan estrechos como sea posible.

Comprueba lo que sabes#

Pregunta 1 de 4

`let x = "Daño"` tiene tipo `string`, pero `const x = "Daño"` tiene tipo `"Daño"`. ¿Por qué?

Tu turno#

El módulo de configuración del Team Builder tiene ROLES como string[] y CONFIG con propiedades ensanchadas. Aplica as const y satisfies para que los tipos sean precisos y TypeScript valide la forma. Cuando lo tengas (o si te atascas), despliega las soluciones y fíjate en el salto de un nivel al siguiente.

Ejercicio · en esta página

Congela y valida el módulo de configuración del Team Builder

El módulo de configuración tiene tipos demasiado anchos: ROLES es string[] y CONFIG tiene sus propiedades ensanchadas. Aplica as const y satisfies para que los tipos sean precisos y TypeScript valide la forma.

Paso 1: Que los tipos sean estrechos

  • Aplicas as const a ROLES: el tipo pasa de string[] a readonly ["Daño", "Tanque", "Apoyo"].
  • Aplicas as const a CONFIG: cada propiedad pasa a ser readonly con su tipo literal (5, "Clasificatoria", "Apoyo").
  • El panel "Problemas" queda vacío.
Ver soluciones
// Solución OK — as const en el array y en el objeto de configuración.

interface Heroe {
  nombre: string;
  rol: string;
  partidas: number;
  victorias: number;
}

type Rol = "Daño" | "Tanque" | "Apoyo";

interface ConfigTeam {
  tamanoEquipo: number;
  modoJuego: string;
  rolPorDefecto: Rol;
}

// as const congela el array: el tipo pasa de string[] a
// readonly ["Daño", "Tanque", "Apoyo"]. Cada elemento es un literal,
// no un string genérico.
const ROLES = ["Daño", "Tanque", "Apoyo"] as const;

// as const congela el objeto: cada propiedad pasa a ser readonly
// y su tipo es el literal exacto (5, "Clasificatoria", "Apoyo")
// en vez de number o string.
// Pero TypeScript no comprueba aquí que cumpla ConfigTeam: eso es trabajo de satisfies.
const CONFIG = {
  tamanoEquipo: 5,
  modoJuego: "Clasificatoria",
  rolPorDefecto: "Apoyo",
} as const;

const equipo: Heroe[] = [
  { nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 },
  { nombre: "Mercy", rol: "Apoyo", partidas: 200, victorias: 155 },
  { nombre: "Reinhardt", rol: "Tanque", partidas: 95, victorias: 61 },
];

console.log("Roles disponibles: " + ROLES.join(", "));
console.log("Modo: " + CONFIG.modoJuego);
console.log("Rol por defecto: " + CONFIG.rolPorDefecto);
console.log("Heroes: " + equipo.map(function(h) { return h.nombre; }).join(", "));

Por qué este nivel

  • as const en ROLES: el array pasa de string[] a readonly ["Daño", "Tanque", "Apoyo"]. Cada posición es un literal, no un string genérico.
  • as const en CONFIG: tamanoEquipo es 5 (no number), rolPorDefecto es "Apoyo" (no string), y todas las propiedades son readonly.
  • El tipo queda preciso, pero no hay validación de forma: si añadieras una clave que no existe en ConfigTeam, TypeScript no diría nada aquí.