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:
{
"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:
{
"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:
// 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.
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.
{
"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.
{
"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.
{
"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:
{
"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 detecta | Qué pasa en el código | Lo que tienes que hacer |
|---|---|---|
noImplicitAny | Un parámetro de función no tiene tipo; TypeScript no puede ayudarte dentro | Anotar el parámetro con su tipo correcto |
strictNullChecks | El retorno de una función oculta que puede ser undefined | Declarar el retorno honesto y gestionar el caso vacío |
strictNullChecks | Un campo opcional se usa sin comprobar si existe | Hacer 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.
Paso 2: Que esté pulido
- mejorHeroe declara su retorno como Heroe | undefined y lo comprueba antes de usar.
- mostrarApodo usa un ternario o un if real para gestionar la ausencia de apodo.
- Cero "as" y cero "any" en el fichero.
Paso 3: Que sea excelente
- Todas las firmas llevan parámetros y retorno anotados como contrato explícito.
- El narrowing de apodo se hace con if (heroe.apodo !== undefined): TypeScript lo razona y dentro del bloque sabe que es string.
- El uso de lider es seguro porque el tipo Heroe | undefined obliga a comprobarlo, no porque seas cuidadoso.
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.
// Solución Mejor: los tres errores se corrigen con comprobaciones reales.
// Nada de "|| string vacío" que oculta la ausencia: se maneja con intención.
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" },
];
// Parámetro anotado: noImplicitAny satisfecho.
// Retorno inferido como number: no es necesario anotarlo aquí.
function calcularWinrate(heroe: Heroe) {
return heroe.victorias / heroe.partidas;
}
// El retorno ahora es honesto: si el array estuviera vacío, reduce sin valor inicial
// lanzaría un error en runtime. Declarar Heroe | undefined nos recuerda ese riesgo
// y nos obliga a comprobarlo donde se use el valor.
function mejorHeroe(lista: Heroe[]): Heroe | undefined {
if (lista.length === 0) return undefined;
return lista.reduce(function(actual, siguiente) {
return siguiente.victorias > actual.victorias ? siguiente : actual;
});
}
// Comprobación con ternario: si apodo existe es string, si no, se devuelve
// un texto descriptivo. TypeScript lo confirma: dentro del bloque verdadero,
// apodo es string (no undefined).
function mostrarApodo(heroe: Heroe): string {
return heroe.apodo
? "Apodo: " + heroe.apodo.toUpperCase()
: heroe.nombre + " no tiene apodo";
}
// --- Muestra el resumen ---
equipo.forEach(function(heroe) {
const wr = (calcularWinrate(heroe) * 100).toFixed(1);
console.log(heroe.nombre + " (" + heroe.rol + "): " + wr + "%");
});
// Ahora el retorno puede ser Heroe | undefined: comprobamos antes de usar.
const lider = mejorHeroe(equipo);
if (lider !== undefined) {
console.log("Lider: " + lider.nombre + " con " + lider.victorias + " victorias");
}
equipo.forEach(function(heroe) {
console.log(mostrarApodo(heroe));
}); Por qué es mejor que el anterior
- mejorHeroe devuelve Heroe | undefined: un tipo honesto que reconoce que la lista puede estar vacía. Eso obliga a comprobar el resultado antes de usarlo, y TypeScript lo refuerza.
- mostrarApodo usa un ternario con intención: si hay apodo lo muestra en mayúsculas; si no, dice explícitamente "sin apodo". Nada de trucos.
- Cero "as", cero "any": la red de seguridad que strict activa se respeta de principio a fin.
// Solución Excelente: cada decisión de tipo tiene una razón, no solo silencia el error.
// Las firmas son contratos que documentan el comportamiento esperado.
// Cero "as", cero "any", cero || "" para tapar un undefined.
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" },
];
// Firma explícita: el contrato dice "recibe un Heroe, devuelve un number".
// Si alguien cambia la lógica interna y el retorno deja de ser number, el error
// salta aquí, en la función, no a tres llamadas de distancia.
function calcularWinrate(heroe: Heroe): number {
return heroe.victorias / heroe.partidas;
}
// Firma honesta: la lista puede estar vacía, el retorno puede ser undefined.
// TypeScript nos obliga a gestionar ese caso donde se consuma la función.
// Eso es exactamente lo que quiere strictNullChecks: que los "puede fallar" no
// queden escondidos.
function mejorHeroe(lista: Heroe[]): Heroe | undefined {
if (lista.length === 0) return undefined;
return lista.reduce(function(actual, siguiente) {
return siguiente.victorias > actual.victorias ? siguiente : actual;
});
}
// Narrowing explícito con un type guard de la forma más legible: el ternario.
// Dentro del bloque verdadero TypeScript sabe que apodo es string (no undefined),
// así que toUpperCase() es seguro SIN necesitar "as" ni forzar nada.
function mostrarApodo(heroe: Heroe): string {
if (heroe.apodo !== undefined) {
// Dentro de este bloque, TypeScript ha estrechado apodo a string.
return heroe.nombre + " — Apodo: " + heroe.apodo.toUpperCase();
}
// En este punto, TypeScript sabe que apodo es undefined.
return heroe.nombre + " — sin apodo";
}
// --- Muestra el resumen ---
equipo.forEach(function(heroe) {
const wr = (calcularWinrate(heroe) * 100).toFixed(1);
console.log(heroe.nombre + " (" + heroe.rol + "): " + wr + "%");
});
// mejorHeroe puede devolver undefined: la comprobación es necesaria y TypeScript
// lo refuerza. Si omites el "if", el acceso a lider.nombre sería un error de tipo.
const lider = mejorHeroe(equipo);
if (lider !== undefined) {
console.log("Lider: " + lider.nombre + " con " + lider.victorias + " victorias");
}
equipo.forEach(function(heroe) {
console.log(mostrarApodo(heroe));
}); Por qué es mejor que el anterior
- Las firmas son contratos: calcularWinrate declara que recibe Heroe y devuelve number. Si alguien cambia la lógica interna y el retorno deja de ser number, el error salta aquí, en la firma, no a tres llamadas de distancia.
- El narrowing de apodo con "if (heroe.apodo !== undefined)" es el patrón que TypeScript razona mejor: dentro del bloque, apodo es string de forma garantizada, sin ningún "as". Eso es exactamente lo que strictNullChecks te da.
- El tipo Heroe | undefined en mejorHeroe no es un detalle: TypeScript no deja usar lider.nombre sin la comprobación previa. La seguridad viene del tipo, no de acordarse de hacerlo.