learning-front

Nivel 5 · TypeScript: JavaScript con red de seguridad

tsconfig: configurar el compilador

strict y sus flags, target, module y las opciones del compilador de TypeScript que de verdad importan en un proyecto real.

Ya sabes escribir TypeScript: anotas tipos, defines interfaces, haces narrowing, usas genéricos y utility types. Pero ¿qué controla que todo ese sistema de tipos se active de verdad en tu proyecto? ¿Qué decide si TypeScript es estricto o permisivo, o a qué versión de JavaScript compila tu código?

Todo eso vive en tsconfig.json: el fichero de configuración del compilador.

Qué es tsconfig.json#

tsconfig.json es el fichero que le dice al compilador de TypeScript cómo comportarse en tu proyecto. Cada vez que ejecutas tsc, o que tu editor type-chequea el código en vivo, lee este fichero para saber qué reglas aplicar.

Un tsconfig.json real y funcional para un proyecto con Vite tiene este aspecto:

json
{
  "compilerOptions": {
    // strict: activa todas las comprobaciones de seguridad de tipos de golpe.
    // En un proyecto nuevo, siempre debe estar a true.
    "strict": true,

    // target: a qué versión de JavaScript compila TypeScript tu código.
    // ESNext es lo más moderno; ES2022 es una opción estable y ampliamente soportada.
    "target": "ES2022",

    // module: qué sistema de módulos usa el código de salida.
    // ESNext genera import/export nativos, que es lo que esperan Vite y los bundlers modernos.
    "module": "ESNext",

    // moduleResolution: cómo resuelve TypeScript los imports.
    // "bundler" es el valor correcto cuando hay un bundler (Vite, esbuild, Webpack) de por medio.
    "moduleResolution": "bundler",

    // lib: qué APIs globales conoce TypeScript (las del navegador, las de ES2022...).
    // "DOM" añade window, document y compañía; "ESNext" añade los métodos modernos de array, string, etc.
    "lib": ["ESNext", "DOM"],

    // noEmit: TypeScript solo comprueba tipos, no genera ficheros .js.
    // El bundler (Vite) se encarga de la compilación real.
    "noEmit": true,

    // skipLibCheck: no comprueba los tipos de las librerías de node_modules.
    // Ahorra tiempo y evita errores en código que no controlas.
    "skipLibCheck": true
  },
  // include: qué ficheros forman parte del proyecto.
  "include": ["src"]
}

La mayoría de proyectos con Vite o Create React App generan un tsconfig.json similar a este. Raramente necesitas tocarlo desde cero: lo que sí conviene entender es qué hace cada opción importante para saber cuándo ajustarla.

strict: el interruptor maestro#

"strict": true es la opción más importante de todo el tsconfig. Con una sola línea activa un conjunto de comprobaciones de seguridad que, sin ella, estarían apagadas:

json
{
  "compilerOptions": {
    // strict: true equivale a activar todas estas flags a la vez:
    // noImplicitAny: no acepta parámetros ni variables sin tipo cuando no puede inferirlos.
    // strictNullChecks: null y undefined son tipos propios; no puedes usarlos sin comprobar.
    // strictFunctionTypes: comprueba que los tipos de parámetros de funciones son compatibles.
    // strictBindCallApply: comprueba los argumentos de bind, call y apply.
    // strictPropertyInitialization: las propiedades de clases deben inicializarse en el constructor.
    // noImplicitThis: this debe tener un tipo conocido dentro de las funciones.
    "strict": true
  }
}

Las dos que más vas a notar en el día a día son noImplicitAny y strictNullChecks.

noImplicitAny: los parámetros tienen que tener tipo#

Cuando TypeScript no puede inferir el tipo de un parámetro —porque no hay ningún valor inicial ni contexto que lo defina—, sin noImplicitAny lo trata como any en silencio. Eso apaga la comprobación de tipos justo donde más falta hace: en la firma de tus funciones.

Con noImplicitAny activo, si no anotas el parámetro, TypeScript lo marca en rojo:

typescript
// Sin noImplicitAny: TypeScript acepta esto y trata "heroe" como any.
// Pierdes toda la ayuda del editor dentro de la función.
function calcularWinrate(heroe) {
  // TypeScript no sabe qué es heroe: no te sugiere campos, no detecta errores.
  return heroe.victorias / heroe.partidas;
}

// Con noImplicitAny: el parámetro debe tener un tipo.
// A partir de aquí TypeScript puede ayudarte dentro de la función.
function calcularWinrate(heroe: Heroe): number {
  return heroe.victorias / heroe.partidas;
}

strictNullChecks: null y undefined son tipos propios#

Sin strictNullChecks, null y undefined se cuelan en cualquier tipo sin avisar. Puedes tener una variable string que en realidad sea undefined, llamar a .toUpperCase() y descubrirlo cuando la app ya está rota en producción.

Con strictNullChecks activo, TypeScript diferencia entre string y string | undefined. Si un campo puede no estar, su tipo lo dice: apodo?: string es string | undefined. Y si intentas usarlo sin comprobar que existe, TypeScript te frena antes de ejecutar.

typescript
interface Heroe {
  nombre: string;
  // apodo?: string significa string | undefined: puede estar o no.
  apodo?: string;
}

const mercy: Heroe = { nombre: "Mercy", rol: "Apoyo" };

// Esto da error con strictNullChecks: apodo puede ser undefined,
// y undefined no tiene toUpperCase.
// mercy.apodo.toUpperCase();

// Así sí: compruebas primero que existe, y dentro TypeScript sabe que es string.
const texto = mercy.apodo ? mercy.apodo.toUpperCase() : "sin apodo";

En el editor de abajo puedes ver strictNullChecks en acción. La isla ya corre en modo strict, así que el error del campo opcional sin comprobar sale en vivo. Descomenta la línea marcada para verlo:

target: a qué JavaScript compilas#

target le dice al compilador de TypeScript a qué versión de JavaScript debe transformar tu código. El TypeScript que escribes siempre es moderno; target controla cómo queda la salida.

json
{
  "compilerOptions": {
    // ES2022: salida JavaScript de ES2022.
    // Los navegadores actuales y Node.js reciente lo soportan sin problemas.
    "target": "ES2022",

    // ESNext: la versión más moderna disponible.
    // Adecuado cuando hay un bundler o transpilador (Vite, esbuild) que se encarga
    // de la compatibilidad final con los navegadores destino.
    "target": "ESNext"
  }
}

El efecto práctico de target afecta principalmente a las características sintácticas de JavaScript: async/await, clases, métodos opcionales, etc. Si pones ES5, TypeScript transforma async/await a una forma equivalente que entienden los navegadores antiguos, las clases a funciones constructoras y las arrow functions a function. Si pones ES2022 o ESNext, la salida queda prácticamente igual a lo que escribes.

¿Cuándo usar cada valor? En la práctica, solo necesitas ES5 si el proyecto debe funcionar en navegadores muy antiguos — piensa en aplicaciones internas bancarias heredadas o en compatibilidad forzada con Internet Explorer 11. En cualquier proyecto nuevo con Vite, ESNext o ES2022 son las opciones correctas: los navegadores actuales los entienden de forma nativa y el bundler se ocupa de la compatibilidad final si hace falta.

module y moduleResolution: cómo se importa el código#

module define qué sistema de módulos usa TypeScript en la salida compilada. moduleResolution define cómo busca los ficheros cuando encuentras un import.

json
{
  "compilerOptions": {
    // module: el sistema de módulos del código de salida.
    // "ESNext": genera import/export nativos (ESM), el estándar actual.
    // "CommonJS": genera require/module.exports, el sistema antiguo de Node.js.
    //   CommonJS usa require() en lugar de import; en proyectos nuevos con Vite no lo usarás.
    "module": "ESNext",

    // moduleResolution: cómo resuelve TypeScript los imports.
    // "bundler": el modo correcto cuando hay Vite, esbuild o Webpack de por medio.
    //   Permite importar sin extensión, usar paths cortos y aprovechar las rutas del package.json.
    // "node16" / "nodenext": el modo para Node.js moderno sin bundler.
    // "node": el modo legacy de Node.js (evítalo en proyectos nuevos).
    "moduleResolution": "bundler"
  }
}

La combinación "module": "ESNext" + "moduleResolution": "bundler" es el estándar para proyectos con Vite. Le dices a TypeScript: “el código de salida usa import/export nativos, y hay un bundler que se encarga de resolver los ficheros”.

lib: qué APIs conoce TypeScript#

lib completa el cuadro: le dice a TypeScript qué APIs globales existen en tu entorno.

json
{
  "compilerOptions": {
    // "DOM": TypeScript conoce window, document, HTMLElement y las APIs del navegador.
    // "ESNext": TypeScript conoce los métodos modernos de array, string, Promise, etc.
    // Sin "DOM", TypeScript no sabe qué es document.querySelector y lo marca como error.
    "lib": ["ESNext", "DOM"]
  }
}

paths: rutas cortas para imports#

paths te permite definir alias de importación (@components/* en lugar de ../../components/*). Existe, pero configurarla de verdad requiere tocar también vite.config.ts para que el bundler resuelva el mismo alias en tiempo de ejecución: son dos ficheros que deben estar sincronizados. La mayoría de proyectos con Vite empiezan sin paths y solo la añaden cuando la profundidad de rutas se vuelve un problema real. Por ahora quédatela en mente; cuando la necesites, la documentación oficial de Vite y TypeScript explica el proceso completo.

Las opciones que de verdad importan en un proyecto real#

La mayoría de los proyectos que vas a encontrar en una empresa usan alguna variante de este tsconfig. Las opciones que vale la pena entender de verdad son cuatro:

json
{
  "compilerOptions": {
    // 1. strict: true — SIEMPRE. Sin ella, la mitad de la red de seguridad está apagada.
    //    Es la opción más importante del fichero.
    "strict": true,

    // 2. target y module — dependen del entorno.
    //    Proyecto con Vite: "target": "ES2022", "module": "ESNext".
    //    Librería npm que también debe funcionar en Node: "module": "CommonJS".
    "target": "ES2022",
    "module": "ESNext",

    // 3. moduleResolution — sigue a module.
    //    Vite u otro bundler: "bundler".
    //    Node.js moderno sin bundler: "node16" o "nodenext".
    "moduleResolution": "bundler",

    // 4. noEmit: true — si el bundler compila, TypeScript solo type-chequea.
    //    Evita conflictos entre la salida de tsc y la de Vite.
    "noEmit": true
  }
}

El resto de opciones (lib, skipLibCheck, paths, include, exclude) son ajustes de conveniencia que el propio Vite, el equipo o el framework ya configura por ti en el tsconfig.json de plantilla. Lo que no deberías cambiar nunca sin motivo: "strict": false. Si alguien en un proyecto real te pide que desactives strict, lo que en realidad está pidiendo es esconder errores que merece la pena ver.

Comprueba lo que sabes#

Pregunta 1 de 5

¿Qué hace la opción `"strict": true` en un tsconfig.json?

Tu turno#

Todo el capítulo ha girado en torno a qué activa "strict": true en un tsconfig. El ejercicio te pone frente a eso de forma directa: el código del Team Builder tiene tres errores que solo aparecen con strict activo, uno por cada flag que más vas a notar en el día a día.

Flag que lo detectaQué pasa en el códigoLo que tienes que hacer
noImplicitAnyUn parámetro de función no tiene tipo; TypeScript no puede ayudarte dentroAnotar el parámetro con su tipo correcto
strictNullChecksEl retorno de una función oculta que puede ser undefinedDeclarar el retorno honesto y gestionar el caso vacío
strictNullChecksUn campo opcional se usa sin comprobar si existeHacer narrowing antes de acceder

Esto es lo que ocurre en la práctica cuando abres un proyecto con "strict": false y lo activas: estos tres patrones aparecen decenas de veces. Saber identificarlos y corregirlos con propiedad —no con as ni any ni @ts-ignore— es exactamente la habilidad que strict: true en el tsconfig exige al equipo.

El panel “Problemas” te los señala uno a uno. Corrígelos hasta que quede vacío.

Ejercicio · en esta página

Arregla lo que strict saca a la luz

El módulo de resumen del Team Builder tiene tres problemas que "strict": true saca a la luz en el tsconfig. Cada error corresponde a una flag concreta: noImplicitAny detecta el parámetro sin tipo, strictNullChecks detecta el retorno que oculta un posible undefined, y strictNullChecks detecta el campo opcional usado sin comprobar. Tu tarea: corregir cada uno hasta que el panel "Problemas" quede vacío, sin "as", sin "any" y sin "@ts-ignore".

Paso 1: Que funcione

  • El parámetro de calcularWinrate tiene su tipo anotado.
  • El acceso a apodo no da error de tipo (aunque sea con un truco como || "").
  • El panel "Problemas" queda vacío y la consola muestra el resumen.
Ver soluciones
// Solución OK: los tres errores quedan corregidos de la forma más directa,
// aunque alguna decisión podría ser más precisa.

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

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

// Se anota el parámetro: noImplicitAny queda satisfecho.
function calcularWinrate(heroe: Heroe) {
  return heroe.victorias / heroe.partidas;
}

// Se mantiene el retorno como Heroe (sin | undefined).
// Funciona porque el array nunca está vacío en este ejercicio,
// pero no modela con precisión que reduce sin valor inicial puede devolver undefined
// si la lista llegara vacía: TypeScript no avisaría.
function mejorHeroe(lista: Heroe[]): Heroe {
  return lista.reduce(function(actual, siguiente) {
    return siguiente.victorias > actual.victorias ? siguiente : actual;
  });
}

// Se usa "|| string vacío" para que apodo no sea undefined antes de toUpperCase.
// Funciona y compila, pero el resultado ("Apodo: ") no es muy informativo
// para los héroes que realmente no tienen apodo.
function mostrarApodo(heroe: Heroe): string {
  // apodo es string | undefined: usamos || "" para dar un string garantizado.
  return "Apodo: " + (heroe.apodo || "").toUpperCase();
}

// --- Muestra el resumen ---

equipo.forEach(function(heroe) {
  const wr = (calcularWinrate(heroe) * 100).toFixed(1);
  console.log(heroe.nombre + " (" + heroe.rol + "): " + wr + "%");
});

const lider = mejorHeroe(equipo);
console.log("Lider: " + lider.nombre + " con " + lider.victorias + " victorias");

equipo.forEach(function(heroe) {
  console.log(mostrarApodo(heroe));
});

Por qué este nivel

  • Anota el parámetro de calcularWinrate y usa "|| string vacío" para apodo: los errores desaparecen y el código funciona.
  • Su límite: "|| """" tapa el undefined pero no lo gestiona — la consola imprime "Apodo: " para los héroes sin apodo, lo que no es muy informativo. Es una chapuza que pasa el type-check sin resolver el problema real.
  • mejorHeroe mantiene Heroe como retorno sin reconocer que la lista podría estar vacía: TypeScript no avisará si ese caso ocurre en producción.