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:
// 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:
// 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:
// 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:
// 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
typeofykeyofen el capítulo siguiente, podrás hacer algo más con un arrayas const: derivar una unión de los literales que contiene ("Daño" | "Tanque" | "Apoyo"a partir del propio array). Por ahora, quédate con queas constcongela 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:
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 const→ tipos 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:
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.
Paso 2: Que la forma esté validada
- Usas satisfies ConfigTeam en CONFIG (sin as const): TypeScript verifica que encaja con la interfaz.
- Los tipos literales se conservan: rolPorDefecto sigue siendo "Apoyo", no el tipo general Rol.
- Entiendes la diferencia frente a la anotación (: ConfigTeam), que ensancharía los tipos.
Paso 3: Tipos estrechos y forma validada a la vez
- Usas as const satisfies ConfigTeam en CONFIG: propiedades readonly, tipos literales y validación de forma en una sola línea.
- Si cambias rolPorDefecto a un rol inválido, el error aparece en la definición, no después.
- ROLES sigue con as const. Todo el módulo de constantes queda sin ningún tipo ancho.
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í.
// Solución MEJOR — satisfies valida la forma del objeto sin ensancharlo.
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 sigue siendo correcto para el array de roles.
const ROLES = ["Daño", "Tanque", "Apoyo"] as const;
// satisfies verifica que CONFIG cumple la forma ConfigTeam,
// pero NO ensancha los tipos: rolPorDefecto sigue siendo el literal "Apoyo",
// no el tipo general Rol. Si escribieras "CasualNoExiste" aquí, daría error.
//
// Diferencia clave con una anotación:
// const CONFIG: ConfigTeam = { ... } → rolPorDefecto sería Rol (ensanchado)
// const CONFIG = { ... } satisfies ConfigTeam → rolPorDefecto sigue siendo "Apoyo"
const CONFIG = {
tamanoEquipo: 5,
modoJuego: "Clasificatoria",
rolPorDefecto: "Apoyo",
} satisfies ConfigTeam;
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é es mejor que el anterior
- satisfies ConfigTeam valida que el objeto encaja con la interfaz: si pones un rol inválido o una clave desconocida, el error aparece en la definición.
- A diferencia de una anotación (const CONFIG: ConfigTeam = …), el tipo inferido conserva los literales: rolPorDefecto sigue siendo "Apoyo", no Rol.
- Límite: las propiedades no son readonly. Con una anotación tampoco lo serían, pero con as const sí. El salto a excelente es combinar ambos.
// Solución EXCELENTE — as const + satisfies combinados: tipos precisos y forma validada.
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: array congelado, cada elemento es un literal readonly.
const ROLES = ["Daño", "Tanque", "Apoyo"] as const;
// as const + satisfies: lo mejor de los dos mundos.
// - as const congela el objeto: nada se ensancha, cada valor es su literal exacto.
// - satisfies ConfigTeam valida que el objeto cumple la forma ANTES de asignarlo:
// si añades una propiedad que no existe en ConfigTeam o cambias rolPorDefecto
// a un rol inválido, el error aparece aquí, en la definición, no más adelante.
// Con solo as const no habría validación de forma.
// Con solo satisfies los literales seguirían siendo precisos pero sin readonly.
// Juntos: tipos estrechos + validación de forma + propiedades inmutables.
const CONFIG = {
tamanoEquipo: 5,
modoJuego: "Clasificatoria",
rolPorDefecto: "Apoyo",
} as const satisfies ConfigTeam;
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é es mejor que el anterior
- as const satisfies ConfigTeam: los dos operadores en una línea. as const congela y satisfies valida. No hay que elegir entre precisión y seguridad.
- Las propiedades son readonly (as const), los tipos son literales (as const) y la forma está comprobada (satisfies). Un valor inválido falla en la definición.
- Este es el patrón habitual en módulos de constantes de empresa: un objeto o array de configuración que nunca debe mutar, que se valida en compilación y cuyos valores concretos siguen siendo accesibles como literales.