25 de febrero de 2026

Closures en TypeScript

Los closures son la base de muchos patrones avanzados en el desarrollo moderno: desde la gestión de estado en React hasta la creación de módulos, programación funcional y la encapsulación de datos privados. Es algo que, como muchas personas, desconocía cómo implementar.

Raúl Pimentel
Raúl Pimentel
[email protected]
Closures en TypeScript

Si llevas algún tiempo trabajando con JavaScript o TypeScript, probablemente ya has utilizado o escuchado el término closure. Es uno de esos conceptos que, a primera vista, puede parecer abstracto o intimidante, pero que en realidad es muy usable y de gran beneficio saber de ello.

Los closures son la base de muchos patrones avanzados en el desarrollo moderno: desde la gestión de estado en React hasta la creación de módulos, programación funcional y la encapsulación de datos privados. Es algo que, como muchas personas, desconocía cómo implementar.

En este pequeño tutorial veremos qué son, cómo funcionan por dentro, por qué deberías usarlos, y cómo aprovecharlos en TypeScript usando tipado estático para hacerlos aún más robustos.

¿Qué son los Closures?

Un closure es una función que recuerda el entorno léxico en el que fue creada, incluso después de que ese entorno haya finalizado su ejecución. En palabras más simples:

Un closure es una función que tiene acceso a variables de su función padre o contenedora, incluso cuando esa función ya terminó de ejecutarse.

Por ejemplo, analicemos el siguiente bloque de código de un EventListener para un botón:

const button = document.getElementById("countButton");
let clickCount = 0;

button.addEventListener("click", () => {
  clickCount++;
  console.log(`Button clicked ${clickCount} times.`);
});

Explicación

  • Tenemos una función anónima que es pasada como parámetro al EventListener de un botón. Esta función es un Closure que tiene acceso a la variable clickCount.
  • Incluso si esta función finaliza su ejecución, el Closure retiene el acceso a clickCount permitiendo incrementar e imprimir en consola su valor cada vez que se hace un clic sobre el botón.

Para entender esto, necesitamos identificar brevemente algunos conceptos relacionados:

Scope (Alcance)

El scope determina desde dónde es accesible una variable. En JavaScript/TypeScript, el scope puede ser global, de módulo, de función o de bloque (con let y const).

Lexical Scope (Scope Léxico)

El scope léxico significa que el alcance de una variable se determina en el momento en que el código es escrito (en tiempo de compilación), no en tiempo de ejecución. Las funciones anidadas tienen acceso al scope de su función padre.

Esto es exactamente lo que hace posible los closures: una función interna “captura” o “cierra” sobre las variables de su función externa, creándose así el closure. Veamos otro ejemplo:

function outerFunction(message: string) {
  function innerFunction() {
    console.log(message);
  }

  return innerFunction;
}

const myFunction = outerFunction("Hello world from Closure");
myFunction(); // Output: 'Hello world from Closure'

En el ejemplo anterior, innerFunction es un closure: recuerda la variable message aunque outerFunction ya terminó su ejecución.

Cómo Funcionan Internamente

Para entender de verdad los closures, necesitamos explorar cómo JavaScript gestiona la memoria y el contexto de ejecución.

1. El Call Stack y los Execution Contexts

Cada vez que se llama a una función, JavaScript crea un nuevo Execution Context (contexto de ejecución) que contiene:

  • El entorno de variables locales (Variable Environment)
  • Una referencia al scope exterior (Outer Environment Reference)
  • El valor de “this”

Este execution context se apila en el Call Stack. Cuando la función termina, su contexto normalmente sería eliminado de memoria. Aquí es donde los closures hacen algo especial.

2. El Closure Record (Environment Record)

Cuando JavaScript detecta que una función interna hace referencia a variables de su función externa, el motor crea lo que se conoce como un Closure Record o Environment Record. Esto es esencialmente un objeto en el heap de memoria que mantiene vivas las variables referenciadas.

Las variables “capturadas” por un closure no se almacenan en el Stack (que se limpia al salir de la función), sino en el Heap, donde persisten mientras el closure siga siendo referenciado.

3. Cadena de Scopes (Scope Chain)

Cuando una función busca una variable, sigue la scope chain (cadena de scopes):

  • Primero busca en su propio scope local
  • Si no la encuentra, sube al scope del closure (función padre)
  • Luego al scope del módulo o script
  • Finalmente al scope global

Esta búsqueda es eficiente porque la cadena se establece en tiempo de compilación (lexical scope), no en tiempo de ejecución.

// Visualizando el Scope chain

function level1() {
  const var1 = "nivel 1";

  function level2() {
    const var2 = "nivel 2";

    function level3() {
      // Tiene acceso a: var3 (propio), var2 (level2), var1 (level1)
      console.log(var1, var2);
    }
    return level3;
  }

  return level2;
}

const fn = level1()();
fn();

Nota: Para poder observar estos scopes basta con agregar un punto de interrupción (debugger) en el script o Source que queremos inspeccionar. Una vez en el punto de interrupción podrás verificar en el panel derecho los diferentes Scopes a que hace referencia.

Closures debugger

4. Closures y Referencias, no Copias

Un detalle crítico: los closures capturan una REFERENCIA a la variable, no una copia de su valor. Esto significa que si la variable cambia después de que el closure fue creado, el closure verá el nuevo valor.

Por qué Usar Closures

Encapsulation (Encapsulación)

La encapsulación es uno de los pilares de la POO. En TypeScript, aunque contamos con modificadores de acceso como private en clases, los closures ofrecen una alternativa funcional más ligera y flexible para casos donde no necesitas toda la estructura de una clase.

State Preservation (Preservación de Estado)

Los closures son perfectos para mantener estado entre llamadas a funciones sin recurrir a variables globales ni a clases. Cada vez que se crea un closure, obtienes un estado independiente y aislado. Ejemplo useState en React:


import React, { useState } from 'react';

function Counter() {
	const [count, setCount] = useState(0);

	return (
	<div>
		<p>{count}</p>
		<button onClick={() => setCount(count + 1)}>Increment</button>
	</div>
	);
}

En este ejemplo, el useState hook inicializa el contador state con un valor de 0. la función setCount es un closure que tiene acceso al valor actual de count. Cuando se hace click en el botón, la función setCount actualiza el estado, causando que el componente Counter realice un re-render con el nuevo estado.

Modularity (Modularidad)

Los closures permiten crear módulos con APIs limpias y bien definidas, exponiendo solo la interfaz pública y ocultando los detalles de implementación. En TypeScript, los closures aún son útiles para crear utilidades y fábricas de funciones:

interface LoggerConfig {
  prefix: string;
  enabled: boolean;
  level: "info" | "warn" | "error";
}

function createLogger(config: LoggerConfig) {
  // Estado privado del módulo
  const logs: string[] = [];

  // Función privada de utilidad
  const formatMessage = (msg: string): string => {
    return `[${config.prefix}] ${new Date().toISOString()} - ${msg}`;
  };

  // API pública
  return {
    log: (message: string): void => {
      if (config.enabled) {
        const formatted = formatMessage(message);
        logs.push(formatted);
      }
    },
    getLogs: (): readonly string[] => [...logs],
    clearLogs: (): void => {
      logs.length = 0;
    },
  };
}

const apiLogger = createLogger({ prefix: "API", enabled: true, level: "info" });
apiLogger.log("Solicitud recibida");
apiLogger.log("Error al enviar la Solicitud");
console.log(apiLogger.getLogs());

En el ejemplo anterior podemos apreciar la fábrica de funciones, las cuales hacen referencia a su scope léxico (closure).

Dynamic Function Generation (Generación Dinámica de Funciones)

Los closures permiten crear funciones especializadas en tiempo de ejecución, basadas en parámetros o configuraciones dinámicas. Esta técnica se conoce como currying o partial application y es central en la programación funcional.

En TypeScript, el sistema de tipos nos permite tipar correctamente estas funciones de orden superior, haciendo el código más seguro y autodocumentado.

// Generación dinámica: validadores configurables

type Validator<T> = (value: T) => boolean;

function createRangeValidator(min: number, max: number): Validator<number> {
  return (value: number): boolean => value >= min && value <= max;
}

function createLengthValidator(
  minLen: number,
  maxLen: number,
): Validator<string> {
  return (value: string): boolean =>
    value.length >= minLen && value.length <= maxLen;
}

// Creando validadores especializados

const isValidAge = createRangeValidator(0, 120);
const isValidScore = createRangeValidator(0, 100);
const isValidUsername = createLengthValidator(3, 20);
console.log(isValidAge(25)); // true
console.log(isValidScore(150)); // false
console.log(isValidUsername("ab")); // false

// Curry con TypeScript: aplicación parcial

function multiply(a: number): (b: number) => number {
  return (b: number) => a * b;
}
const double = multiply(2);
const triple = multiply(3);
const tenX = multiply(10);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(tenX(5)); // 50

En la anterior serie de ejemplos nos encontramos con closures que aprovechan el sistema de tipos, así como el uso Kurrying (la técnica de definir funciones atadas a múltiples parámetros como una serie de funciones anidadas que solo esperan un parámetro fue popularizada por el matemático Haskell Curry, a dicha cadena de funciones anidadas se les llama funciones curry o curried functions.)

Conclusiones

Los closures son una de las herramientas más poderosas y versátiles de JavaScript y TypeScript.

  • Un closure es una función que recuerda el entorno léxico en el que fue creada, capturando referencias a las variables de su scope padre.
  • Internamente, el motor de JavaScript mantiene vivas las variables capturadas en el Heap mediante un Closure Record, siguiendo la scope chain para resolver referencias.
  • La encapsulación mediante closures ofrece privacidad.
  • La preservación de estado permite crear funciones con memoria propia, cada instancia con su propio estado aislado.
  • La modularidad a través de closures nos permite definir APIs limpias con implementaciones ocultas, un patrón fundamental en el desarrollo profesional.
  • La generación dinámica de funciones, mediante currying y aplicación parcial, abre la puerta a la programación funcional en TypeScript.

TypeScript potencia los closures aún más al añadir tipado estático, lo que nos permite capturar errores en tiempo de compilación, autodocumentar las APIs públicas de nuestros módulos y aprovechar el IntelliSense para una experiencia de desarrollo superior.

La próxima vez que escribas un callback, un hook de React, un middleware o una función de orden superior, recuerda: estás usando closures.