Configuración de tsconfig.json: Opciones Clave para Proyectos TypeScript

Todo proyecto basado en TypeScript incorpora un archivo fundamental en su directorio raíz: tsconfig.json. Este archivo es el núcleo de la configuración para el compilador de TypeScript (tsc), dictando su comportamiento, como qué archivos deben procesarse o cuáles deben ignorarse.

A continuación, se presenta un ejemplo típico de un archivo tsconfig.json, despojando las configuraciones específicas de frameworks para mantener el foco en las opciones universales de TypeScript:

{
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./dist/compiled-ts",
    "sourceMap": true,
    "declaration": false,
    "downlevelIteration": true,
    "experimentalDecorators": true,
    "moduleResolution": "node",
    "importHelpers": true,
    "target": "es2017",
    "module": "es2020",
    "lib": [
      "es2018",
      "dom"
    ]
  }
}

Opciones include, exclude y files

Estas opciones determinan qué archivos son tenidos en cuenta por el compilador.

La propiedad include es una lista blanca (whitelist) de rutas de archivos a compilar. Por ejemplo, para incluir todos los archivos .ts o .tsx de los directorios src y tests:

{
    "compilerOptions": {
        // ... otras opciones
    },
    "include": ["./src/**/*", "./tests/**/*.tsx?"]
}

En este ejemplo, utilizamos dos comodines:

  • **/: Coincide con cualquier subdirectorio, incluyendo el separador de directorios /. Esto permite incluir archivos en cualquier profundidad.
  • *: Coincide con cualquier cadena de caracteres (excepto el separador de directorios /).
  • ?: Coincide con un único carácter (excepto el separador de directorios /).

Así, ./src/**/* incluye cualquier archivo dentro de src y sus subdirectorios, y ./tests/**/*.tsx? incluye archivos .ts o .tsx en tests y sus subdirectorios.

Por otro lado, exclude funciona como una lista negra (blacklist), especificando archivos que el compilador debe ignorar. Comúnmente se usa para excluir el directorio node_modules:

{
    "compilerOptions": {
        // ... otras opciones
    },
    "include": ["./src/**/*"],
    "exclude": ["node_modules/**/*", "./dist/**/*"]
}

Es importante notar que si un archivo excluido es importado por un archivo incluido, el compilador aún lo procesará. Esto es un comportamiento esperado, ya que necesita resolver todas las dependencias.

La opción files es similar a include, pero solo acepta rutas de archivo explícitas (sin comodines). Es útil para proyectos con un punto de entrada único o un conjunto muy específico de archivos raíz:

{
    "compilerOptions": {
        // ... otras opciones
    },
    "files": ["./src/main.ts", "./src/utils/types.d.ts"]
}

La Propiedad extends

La propiedad extends permite que un archivo tsconfig.json herede configuraciones de otro. Esto es muy útil para mantener una base de configuración común y luego especializarla para diferentes entornos o propósitos (por ejemplo, compilación de aplicación versus compilación de pruebas).

Consideremos un escenario donde tenemos una configuración base y dos configuraciones derivadas:

tsconfig.base.json (Configuración base)

{
  "compilerOptions": {
    "target": "es2017",
    "module": "es2020",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist/base"
  }
}

tsconfig.app.json (Configuración para la aplicación, extiende la base)

{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "outDir": "./dist/app",
    "declaration": false,
    "types": ["node"]
  },
  "files": [
    "src/index.ts",
    "src/polyfills.ts"
  ],
  "include": [
    "src/**/*.d.ts"
  ]
}

tsconfig.test.json (Configuración para pruebas, extiende la base)

{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "outDir": "./dist/test",
    "types": ["jest", "node"]
  },
  "include": [
    "src/**/*.spec.ts",
    "src/**/*.d.ts"
  ]
}

Este patrón evita la duplicación de configuraciones y facilita la gestión de proyectos complejos con múltiples objetivos de compilación.

Opciones dentro de compilerOptions

El objeto compilerOptions contiene la mayoría de las configuraciones que afectan directamente cómo TypeScript compila el código.

compilerOptions.allowUnreachableCode

Controla si el compilador permite código que nunca se ejecutará. Puede ser true, false o undefined (valor por defecto, que emite una advertencia).

{
    "compilerOptions": {
        "allowUnreachableCode": false
    }
}

Con allowUnreachableCode: false, el siguiente código generará un error de compilación:

const calcularValor = (): number => {
    return 100;
    console.log('Esta línea es inalcanzable.'); // Error de compilación si allowUnreachableCode es false
};

compilerOptions.allowUnusedLabels

Esta opción, poco común en la práctica, se refiere al uso de etiquetas (labels) en JavaScript. Controla si el compilador permite etiquetas declaradas pero no utilizadas. Los valores son true, false o undefined (por defecto, que emite una advertencia).

function procesarElementos() {
    console.log('Iniciando procesamiento...');
    identificadorLoop: for (let x = 0; x < 5; x++) {
        for (let y = 0; y < 5; y++) {
            if (x * y > 10) {
                // break identificadorLoop; // Si no se usa, y allowUnusedLabels es false, habrá un error.
            }
            console.log(`Coordenada: (${x}, ${y})`);
        }
    }
}

compilerOptions.alwaysStrict

Cuando se establece en true (que es el valor por defecto en muchos contextos), asegura que el código JavaScript generado se adhiere al "modo estricto" de ECMAScript, es decir, incluye "use strict"; al inicio de los archivos.

compilerOptions.exactOptionalProperties

Introducida en TypeScript 4.4, esta opción (desactivada por defecto) impone un chequeo más estricto en las propiedades opcionales. Si una propiedad es opcional (prop?: Type), solo puede omitirse o establecerse con un valor de Type. No se le puede asignar undefined explícitamente a menos que undefined sea parte de su tipo.

interface ConfiguracionUsuario {
  theme?: string;
}

// Con "exactOptionalProperties": false (por defecto):
const conf1: ConfiguracionUsuario = {};
conf1.theme = 'dark';
conf1.theme = undefined; // Permitido

// Con "exactOptionalProperties": true:
const conf2: ConfiguracionUsuario = {};
conf2.theme = 'light';
// conf2.theme = undefined; // Error: Type 'undefined' is not assignable to type 'string'.
                           // Para permitirlo, el tipo debería ser 'theme?: string | undefined;'.

// Error con "exactOptionalProperties": true:
// const conf3: ConfiguracionUsuario = { theme: undefined };
// Type '{ theme: undefined; }' is not assignable to type 'ConfiguracionUsuario'.

compilerOptions.downlevelIteration

Esta bandera controla cómo TypeScript transpilas construcciones de iteración modernas (como for...of y la propagación de arrays [...arr]) a versiones de JavaScript más antiguas (ES3/ES5). Si se desactiva (por defecto en algunos targets bajos), estas construcciones podrían no funcionar correctamente con ciertos tipos de datos (como caracteres Unicode complejos) en entornos antiguos.

Consideremos un ejemplo con caracteres emoji:

const mensaje = `Hola 👋 mundo!`;
for (const caracter of mensaje) {
  console.log(caracter);
}

Si target es es5 y downlevelIteration está desactivado, el bucle for...of se transpilará a una iteración basada en índices (for (var i = 0; i < mensaje.length; i++)). Dado que algunos caracteres Unicode ocupan más de un "punto de código" en UTF-16 (como los emojis), un solo emoji puede ser interpretado como dos caracteres separados, resultando en una salida incorrecta o "mojis rotos".

Al activar downlevelIteration: true, TypeScript generará código auxiliar que gestiona la iteración de manera más robusta, asegurando que los caracteres Unicode complejos se traten correctamente, incluso en entornos ES3/ES5. El código JavaScript generado incluirá una función auxiliar (ej. __values) para manejar la iteración a través de la interfaz Symbol.iterator.

compilerOptions.importHelpers

Cuando TypeScript transpila código a versiones de JavaScript más antiguas, a menudo necesita insertar funciones auxiliares (helpers) para emular características modernas (como las mencionadas en downlevelIteration o decoradores). Por defecto, estas funciones se insertan directamente en cada archivo donde se necesitan, lo que puede aumentar el tamaño del código y crear duplicados.

Si se activa importHelpers: true, en lugar de insertar el código auxiliar repetidamente, TypeScript generará una importación a un módulo centralizado que contiene todas estas funciones. Este módulo es generalmente tslib. Esto reduce el tamaño del paquete final, especialmente en proyectos grandes.

Ejemplo TypeScript:

export const procesarNombres = (nombres: string[]) => {
  for (const nombre of nombres) {
    console.log(`Procesando: ${nombre}`);
  }
};

Con importHelpers: false (y downlevelIteration: true y target: es5), el código auxiliar __values se insertaría en el archivo.

Con importHelpers: true, el código resultante importará el helper:

import { __values } from "tslib";

export var procesarNombres = function (nombres) {
    var e_1, _a;
    try {
        for (var nombres_1 = __values(nombres), nombres_1_1 = nombres_1.next(); !nombres_1_1.done; nombres_1_1 = nombres_1.next()) {
            var nombre = nombres_1_1.value;
            console.log("Procesando: " + nombre);
        }
    }
    catch (e_1_1) { e_1 = { error: e_1_1 }; }
    finally {
        try {
            if (nombres_1_1 && !nombres_1_1.done && (_a = nombres_1.return)) _a.call(nombres_1);
        }
        finally { if (e_1) throw e_1.error; }
    }
};

Para que esto funcione, tslib debe estar instalado en el proyecto (npm install tslib).

compilerOptions.strict

Esta es una bandera maestra que activa un conjunto de opciones de verificación de tipos estrictas. Habilitarla es una práctica recomendada para la mayoría de los proyectos modernos, ya que mejora significativamente la seguridad y robustez del código. Las opciones que activa incluyen:

  • strictNullChecks
  • noImplicitAny
  • strictFunctionTypes
  • strictPropertyInitialization
  • strictBindCallApply
  • alwaysStrict
  • noImplicitThis
  • useUnknownInCatchVariables

Aunque strict: true habilita todas estas, aún se pueden desactivar individualmente si es necesario.

compilerOptions.strictBindCallApply

Si está activa, TypeScript verificará la compatibilidad de los argumentos cuando se usen los métodos bind, call y apply en funciones.

function mostrarCoordenadas(x: number, y: number) {
    console.log(`X: ${x}, Y: ${y}`);
}

// Con strictBindCallApply: true, esto resultaría en un error de compilación:
// Argument of type 'string' is not assignable to parameter of type 'number'.
// mostrarCoordenadas.call(null, 'cinco', 10);

// Esto sería válido:
mostrarCoordenadas.apply(null, [5, 10]);

compilerOptions.strictFunctionTypes

Habilita un chequeo más riguroso en la asignación de tipos de función. Esto afecta a la contravarianza y covarianza de los parámetros de función. Por ejemplo:

type EventHandler = (data: string | number) => void;

const handleStringEvent = (message: string) => {
  console.log('Mensaje:', message);
};

// Con strictFunctionTypes: true, esto causaría un error:
// Type '(message: string) => void' is not assignable to type 'EventHandler'.
// Parameters of types '(data: string | number)' and '(message: string)' are incompatible.
// const myHandler: EventHandler = handleStringEvent;

// Si la función acepta un tipo más amplio (superconjunto), la asignación es válida:
const handleGenericEvent = (data: string | number) => {
  console.log('Evento recibido:', data);
};
const anotherHandler: EventHandler = handleGenericEvent; // Válido

compilerOptions.strictNullChecks

Es una de las ocpiones más influyentes. Obliga a que los valores null y undefined se manejen explícitamente en los tipos. Sin esta opción, null y undefined son asignables a cualquier tipo, lo que puede llevar a errores en tiempo de ejecución. Con ella, debe usar uniones de tipos (Type | null | undefined) para indicar que una variable puede ser nula o indefinida.

let usuarioLogueado: string | null = null;

// Sin strictNullChecks: La línea de abajo compilaría, pero causaría un error en tiempo de ejecución.
// Con strictNullChecks: Error: Object is possibly 'null'.
// console.log(usuarioLogueado.toUpperCase());

// Forma correcta con strictNullChecks:
if (usuarioLogueado) {
    console.log(usuarioLogueado.toUpperCase());
} else {
    console.log('Usuario no logueado.');
}

compilerOptions.strictPropertyInitialization

Asegura que todas las propiedades de una clase se inicialicen en el constructor o en la declaración de la propiedad. Solo aplica a propiedades que no tienen un tipo undefined explícito en su definición.

class Configuracion {
  // Con strictPropertyInitialization: true, esto generaría un error:
  // Property 'nombreApp' has no initializer and is not definitely assigned in the constructor.
  // nombreApp: string;

  // Opciones válidas:
  version: string = '1.0.0'; // Inicialización directa
  baseUrl: string;

  constructor(url: string) {
    this.baseUrl = url; // Inicialización en el constructor
  }

  // O usando un operador de aserción no nulo (¡!) si sabes que se inicializará fuera del constructor
  // (usar con precaución):
  puerto!: number;
}

compilerOptions.noImplicitAny

Si está activa, TypeScript emitirá un error cada vez que infiera el tipo any implícitamente, es decir, cuando no se puede determinar un tipo más específico y el desarrollador no ha especificado uno. Esto fomenta una tipificación más explícita y previene errores.

function procesarDatos(datos: string) { // Correcto: tipo explícito
    console.log('Datos:', datos);
}

// Con noImplicitAny: true, esto generaría un error:
// Parameter 'input' implicitly has an 'any' type.
// function procesarEntrada(input) {
//     console.log(input);
// }

procesarDatos('ejemplo');

Esta opción se activa automáticamente con strict: true.

compilerOptions.noImplicitOverride

Introducida en TypeScript 4.3, esta opción requiere que los métodos que sobrescriben un método de una clase base deben usar explícitamente el modificador override. Esto mejora la legibilidad y previene errores comunes donde un método se pretende sobrescribir pero no coincide con la firma del método base.

class Vehiculo {
    encender() { console.log('Vehículo encendido'); }
    detener() { console.log('Vehículo detenido'); }
}

class Coche extends Vehiculo {
    // Con noImplicitOverride: true, la línea de abajo generaría un error:
    // This member must have an 'override' modifier because it overrides a member in the base class 'Vehiculo'.
    // encender() { console.log('Coche encendido'); }

    // Forma correcta:
    override encender() { console.log('Coche encendido y listo'); }
    override detener() { console.log('Coche detenido y seguro'); }
}

compilerOptions.noImplicitReturns

Habilita la verificación para asegurar que todas las rutas de código en una función devuelven un valor cuando se espera uno. Si una función tiene ramas condicionales, todas las ramas deben retornar un valor del tipo esperado, o no retornar ningún valor si el tipo de retorno es void o una unión con undefined.

const obtenerMensaje = (incluirPrefijo: boolean): string => {
    if (incluirPrefijo) {
        return 'PREFIJO: Mensaje completo';
    }
    // Con noImplicitReturns: true, esta ruta de código sin un 'return' generaría un error.
    // 'Not all code paths return a value.'
    // return 'Mensaje simple'; // Necesario para corregir el error
};

// Alternativa válida con un tipo de retorno que incluye 'undefined':
const obtenerDetalle = (mostrar: boolean): string | undefined => {
    if (mostrar) {
        return 'Detalle disponible';
    }
    // No se necesita 'return' explícito si el tipo de retorno lo permite
};

compilerOptions.noImplicitThis

Cuando está activa, TypeScript emitirá un error cuando el valor de this en una función tenga un tipo implícito any. Esto ayuda a evitar errores comunes relacionados con el contexto de this en JavaScript, especialmente en funcionse que no son métodos de clase.

class Punto {
    constructor(public x: number, public y: number) {}

    mover(dx: number, dy: number) {
        this.x += dx;
        this.y += dy;
    }
}

// Con noImplicitThis: true, la siguiente función daría un error si 'this' no está tipado explícitamente:
// function crearPuntoAntiguo(coordX: number, coordY: number) {
//     this.x = coordX; // Error: 'this' implicitly has type 'any'.
//     this.y = coordY;
// }

// Forma correcta (usando una clase o tipando 'this'):
const miPunto = new Punto(10, 20);
miPunto.mover(5, -3);
console.log(`Nuevo punto: (${miPunto.x}, ${miPunto.y})`);

compilerOptions.noPropertyAccessFromIndexSignature

Introducida en TypeScript 4.2, esta opción previene el acceso a propiedades con notación de punto (obj.prop) para miembros que solo están definidos por una firma de índice ([key: string]: Type). Obliga a usar la notación de corchetes (obj['prop']) para tales propiedades, lo que mejora la claridad sobre cómo se accede a ellas y puede ayudar a pervenir errores de tipo en tiempo de ejecución.

interface DatosGenericos {
  [key: string]: string | number;
  id: string; // Propiedad explícita
}

const item: DatosGenericos = {
  id: 'abc-123',
  nombre: 'Artículo X',
  precio: 99.99
};

console.log(item.id); // Acceso normal a propiedad explícita, siempre permitido

// Con noPropertyAccessFromIndexSignature: true, esto generaría un error:
// Property 'nombre' comes from an index signature, so it must be accessed with ['nombre'].
// console.log(item.nombre);

// Forma correcta para propiedades de firma de índice:
console.log(item['nombre']);
console.log(item['precio']);

El motivo principal es que el acceso con notación de punto a una firma de índice puede ser ambiguo y potencialmente inseguro, ya que la propiedad podría no existir en tiempo de ejecución si el objeto no la tiene. Al forzar la notación de corchetes, se enfatiza que el acceso es dinámico.

compilerOptions.noUncheckedIndexedAccess

Introducida en TypeScript 4.1, esta opción hace que TypeScript infiera un tipo con undefined añadido para los accesos a elementos a través de firmas de índice. Esto significa que si accedes a un elemento de un array o un objeto con una firma de índice, el tipo resultante incluirá undefined, forzándote a manejar explícitamente la posibilidad de que el elemento no exista.

const miArray = ['primero', 'segundo'];

// Sin noUncheckedIndexedAccess: tipo de itemUno sería 'string'
// Con noUncheckedIndexedAccess: tipo de itemUno sería 'string | undefined'
const itemUno = miArray[0];

// Con noUncheckedIndexedAccess: el tipo de itemInexistente es 'string | undefined'
const itemInexistente = miArray[2];

// Con noUncheckedIndexedAccess:
// console.log(itemInexistente.toUpperCase()); // Error: Object is possibly 'undefined'.

if (itemInexistente !== undefined) {
    console.log(itemInexistente.toUpperCase()); // Válido
}

Esta opción, junto con strictNullChecks, ayuda a prevenir errores de acceso a propiedades de valores undefined.

compilerOptions.noUnusedLocals

Cuando está activa, TypeScript emitirá un error para cualquier variable local declarada que nunca se lea. Esto ayuda a mantener el código limpio y a identificar código muerto o errores tipográficos.

const miFuncion = () => {
    const valorCalculado = 123; // Con noUnusedLocals: true, esto generaría un error:
                               // ''valorCalculado' is declared but its value is never read.'
    console.log('Ejecutando función.');
    // console.log(valorCalculado); // Si se usa, el error desaparece
};

compilerOptions.noUnusedParameters

Similar a noUnusedLocals, pero aplicada a parámetros de función. Emite un error si un parámetro de función se declara pero nunca se utiliza dentro del cuerpo de la función.

const saludarUsuario = (nombre: string, edad: number) => {
    // Con noUnusedParameters: true, 'edad' generaría un error:
    // ''edad' is declared but its value is never read.'
    console.log(`Hola, ${nombre}!`);
};

// Para evitar el error, si el parámetro no se usa, se puede prefijar con un guion bajo:
const logEvento = (_eventName: string, details: object) => {
    console.log('Detalles del evento:', details);
};

compilerOptions.useUnknownInCatchVariables

Introducida en TypeScript 4.4, esta opción cambia el tipo por defecto de las variables de las cláusulas catch de any a unknown. Esto es más seguro, ya que unknown te obliga a realizar una verificación de tipo antes de interactuar con el error, previendo errores de acceso a propiedades en objetos que podrían no tenerlas (a diferencia de any, que permite cualquier operación).

try {
  JSON.parse('datos inválidos');
} catch (error) {
  // Sin useUnknownInCatchVariables: 'error' es de tipo 'any'.
  // Con useUnknownInCatchVariables: 'error' es de tipo 'unknown'.

  // Si 'error' es 'unknown', esto generaría un error de compilación:
  // error.message; // Error: Object is of type 'unknown'.

  // La forma correcta de manejarlo es verificar el tipo:
  if (error instanceof Error) {
    console.error('Ha ocurrido un error:', error.message);
  } else {
    console.error('Un error desconocido ocurrió:', error);
  }
}

Etiquetas: TypeScript tsconfig compilerOptions TypeChecking JavaScript

Publicado el 8-15 23:41