Skip to content

📙 Clase 13 — Organizar un proyecto web con TypeScript (src + public)

TypeScript · 2026-08-06 · Carpeta: 02-Ejercicios/SitioWeb ⬅️ Volver al índice de clases

🎯 Qué aprendí

  • Por qué mezclar todo en una sola carpeta (como hice en la Clase 12) deja de ser cómodo apenas el proyecto crece, y cómo separar código fuente (src/) de archivos públicos (public/).
  • Cómo usar outDir en tsconfig.json para que tsc compile hacia otra carpeta, en vez de dejar el .js al lado del .ts.
  • La diferencia entre tsc archivo.ts (ignora el tsconfig.json), tsc a secas (lo toma automático si estás parado en esa carpeta) y tsc -p ruta/tsconfig.json (funciona desde cualquier carpeta).
  • Que outDir preserva la estructura de subcarpetas de src/ dentro de public/.
  • Un gotcha real y no obvio: module: "CommonJS" (el que trae mi propio proyecto) compila import/export a require()/exports — que tampoco corre en el navegador, ni con type="module" (eso solo entiende ES Modules, no CommonJS).

📚 Definiciones clave

TérminoQué esEjemplo de esta clase
src/Carpeta con el código fuente (.ts) — no se sube al servidorsrc/main.ts
public/Carpeta con lo que se despliega (HTML, CSS, .js compilado)public/index.html
outDirOpción de tsconfig.json: a dónde tsc manda el .js compilado"outDir": "../public/scripts"
tsc -p archivoCompila usando ESE tsconfig.json, sin importar desde qué carpeta lo corrastsc -p src/tsconfig.json

📖 PARTE TEÓRICA

🗂️ 1. Por qué separar src/ de public/

En la Clase 12 todo vivía junto: index.html, main.ts y main.js en la misma carpeta. Funciona para un ejemplo chico, pero mezclar código fuente con lo que se despliega tiene un costo real:

  • Seguridad: si subes toda la carpeta a un servidor, subes también tu .ts — código fuente que nadie necesita ver desde afuera.
  • Despliegue confuso: ¿qué archivos subo, todos o solo algunos? Con todo junto, no hay una respuesta clara.
  • Se vuelve un desorden apenas agregas más de 2-3 archivos.

La solución: dos carpetas con responsabilidades distintas.

sitio-web/
├── public/              ← esto SÍ se despliega
│   ├── index.html
│   └── scripts/
│       └── main.js      ← generado por tsc, no se edita a mano
└── src/                 ← esto NO se despliega
    ├── main.ts
    └── tsconfig.json

💡 La regla mental es simple: src/ es donde escribo, public/ es lo que subo. Nunca edito nada dentro de public/scripts/ a mano — si lo hago, se pierde en el próximo tsc.

⚙️ 2. outDir: decirle a tsc dónde poner el .js

Por defecto, tsc archivo.ts deja el .js al lado del .ts. Para mandarlo a otra carpeta, se usa outDir dentro de tsconfig.json:

json
{
  "compilerOptions": {
    "outDir": "../public/scripts",
    "target": "ES6",
    "module": "CommonJS",
    "strict": true
  }
}

Este es el tsconfig.json real de 02-Ejercicios/SitioWeb/src/. La ruta de outDir es relativa al propio tsconfig.json, no a la carpeta desde donde corras el comando — por eso ../public/scripts sube un nivel (de src/ a la raíz) y baja a public/scripts.

⚠️ Antes de tener un tsconfig.json, tsc archivo.ts compila en el lugar de siempre. outDir solo aplica cuando tsc usa la configuración — es decir, cuando NO le das el nombre del archivo directamente (ver la sección 4, gotcha real de tu proyecto).

📦 3. Estructura real de este proyecto

02-Ejercicios/SitioWeb/
├── public/
│   ├── index.html
│   └── scripts/
│       └── main.js          ← generado, no se toca a mano
└── src/
    ├── main.ts
    └── tsconfig.json
typescript
// src/main.ts
console.log("Hola desde mi explorador");
html
<!-- public/index.html -->
<script src="./scripts/main.js"></script>

Para compilar, parado en src/:

bash
cd 02-Ejercicios/SitioWeb/src
tsc

Verificado en terminal — tsc (sin argumentos) detecta el tsconfig.json de la carpeta automáticamente y respeta su outDir:

zsh · SitioWeb/src
$ tsc
$ ls ../public/scripts
main.js

⚠️ 4. Gotcha real: tsc main.ts ignora tu tsconfig.json

Este es un error que sí me salió al probarlo en este proyecto. Si le das a tsc el nombre del archivo directamente (como en la Clase 12, donde no había tsconfig.json), y ya existe un tsconfig.json en esa carpeta, tsc se niega a adivinar cuál configuración quieres y truena:

zsh · SitioWeb/src
$ tsc main.ts
error TS5112: tsconfig.json is present but will not be loaded if files are specified on commandline. Use '--ignoreConfig' to skip this error.

📝 Esto explica por qué en la Clase 9 la carpeta Genericos SÍ dejaba compilar tsc archivo.ts sin problema: esa carpeta no tenía tsconfig.json. En cuanto agregas uno, TypeScript exige que lo uses de forma explícita — no mezcla "archivo por nombre" con "configuración por archivo".

Hay tres formas correctas de compilar cuando existe un tsconfig.json:

ComandoDesde dónde funcionaQué hace
tsc (sin nada)Solo si estás parado en la carpeta del tsconfig.jsonLo detecta automático
tsc -p tsconfig.jsonDesde la misma carpetaLo carga explícitamente
tsc -p src/tsconfig.jsonDesde cualquier carpeta (ej. la raíz del proyecto)Le das la ruta completa

Verificado: correr tsc -p src/tsconfig.json parado en la raíz del proyecto (SitioWeb/, no SitioWeb/src/) compila exactamente igual — outDir sigue resolviendo relativo al tsconfig.json, sin importar tu carpeta actual:

zsh · SitioWeb
$ tsc -p src/tsconfig.json
$ ls public/scripts
main.js

🌐 5. Verificar que sigue corriendo en el navegador

La actualización de la ruta en el HTML es lo que conecta todo:

html
<!-- antes (Clase 12, todo junto) -->
<script src="main.js"></script>

<!-- ahora (Clase 13, main.js vive en scripts/) -->
<script src="./scripts/main.js"></script>

Verificado sirviendo public/ en un localhost real y leyendo la consola de Chrome:

Chrome DevTools · Console
Hola desde mi explorador

⚠️ Si olvidas actualizar el <script src> y lo dejas apuntando a main.js (sin la subcarpeta scripts/), el navegador busca el archivo en el lugar equivocado. Verificado: GET http://localhost/main.js responde 404 — el main.js real está en scripts/main.js, un nivel más adentro.


💻 PARTE PRÁCTICA

Archivos trabajados: 02-Ejercicios/SitioWeb/src/main.ts, 02-Ejercicios/SitioWeb/src/tsconfig.json y 02-Ejercicios/SitioWeb/public/index.html.

json
// src/tsconfig.json
{
  "compilerOptions": {
    "outDir": "../public/scripts",
    "target": "ES6",
    "module": "CommonJS",
    "strict": true
  }
}
bash
cd 02-Ejercicios/SitioWeb/src
tsc

Salida real verificada — main.ts compiló hacia public/scripts/main.js (no al lado de main.ts), y el navegador lo cargó sin problema desde ahí:

zsh · SitioWeb
$ find . -type f
./public/index.html
./public/scripts/main.js
./src/main.ts
./src/tsconfig.json

Todo verificado y corriendo. ✅


🏋️ EJERCICIOS CON SOLUCIÓN

Ejercicio 1 — Cambiar el mensaje y recompilar con tsc a secas

Cambia el texto del console.log en src/main.ts, y recompílalo parado en src/ con tsc (sin argumentos, sin -p). Confirma el cambio en la consola del navegador.

💡 ¿Sabías que…? — `tsc` a secas también respeta `outDir`

No hace falta escribir -p tsconfig.json si ya estás parado en la misma carpeta que el tsconfig.json: tsc sin argumentos lo detecta solo y aplica todas sus opciones, incluyendo outDir. Ejemplo de referencia:

bash
cd mi-proyecto/src   # aquí vive tsconfig.json
tsc                  # detecta tsconfig.json automáticamente
Ver solución
typescript
// src/main.ts
console.log("Hola, este mensaje lo cambié yo");
bash
cd 02-Ejercicios/SitioWeb/src
tsc

Consola del navegador tras recargar (sirviendo public/index.html):

Chrome DevTools · Console
Hola, este mensaje lo cambié yo

Ejercicio 2 — Cambiar outDir a otra carpeta

Modifica outDir en tsconfig.json de "../public/scripts" a "../public/build", recompila, y confirma en qué carpeta aparece el .js esta vez.

💡 ¿Sabías que…? — `outDir` no crea el HTML, solo mueve el `.js`

Cambiar outDir no actualiza tu index.html automáticamente — sigue apuntando a la ruta vieja hasta que tú la cambies a mano. outDir solo controla dónde tsc deja el .js de salida. Ejemplo de referencia:

json
{ "compilerOptions": { "outDir": "../dist" } }
Ver solución
json
// src/tsconfig.json
{
  "compilerOptions": {
    "outDir": "../public/build",
    "target": "ES6",
    "module": "CommonJS",
    "strict": true
  }
}
bash
tsc

Terminal esperada:

zsh · SitioWeb/src
$ tsc
$ ls ../public/build
main.js

💡 Si ahora abres el HTML, seguirá roto (sigue apuntando a scripts/main.js, que ya no se regenera) hasta que actualices el <script src> a ./build/main.js — o reviertas outDir a como estaba.

Ejercicio 3 — Forzar el error TS5112

Antes de correrlo: si tu carpeta tiene tsconfig.json y corres tsc main.ts (con el nombre del archivo), ¿qué esperas que pase? Anticipa tu respuesta y compruébala.

💡 ¿Sabías que…? — darle un nombre a `tsc` cambia sus reglas

tsc tiene dos modos que no se mezclan: "usa mi tsconfig.json" (sin nombres de archivo) o "ignora cualquier config y compila justo este archivo con los valores por defecto" (con nombre de archivo). Si detecta un tsconfig.json presente Y le das un nombre de archivo a la vez, asume que es un descuido tuyo y prefiere avisarte en vez de adivinar. Ejemplo de referencia: la Clase 9 documentó el error TS5112 la primera vez que apareció, en un contexto distinto.

Ver solución
bash
cd 02-Ejercicios/SitioWeb/src
tsc main.ts

Terminal real:

zsh · SitioWeb/src
$ tsc main.ts
error TS5112: tsconfig.json is present but will not be loaded if files are specified on commandline. Use '--ignoreConfig' to skip this error.

💡 La solución es usar tsc (sin nombre) o tsc -p tsconfig.json — nunca mezclar nombre de archivo con un tsconfig.json presente.

Ejercicio 4 — Compilar con -p desde la raíz del proyecto

Sin moverte a src/, desde la carpeta raíz SitioWeb/, compila usando tsc -p src/tsconfig.json. Confirma que outDir sigue funcionando igual.

💡 ¿Sabías que…? — `outDir` es relativo al `tsconfig.json`, no a tu terminal

Sin importar desde qué carpeta ejecutes el comando, las rutas dentro de tsconfig.json (como outDir) siempre se resuelven relativas al propio archivotsconfig.json — no a tu directorio actual de terminal. Por eso -p funciona desde cualquier lado. Ejemplo de referencia:

bash
# ambos comandos generan el MISMO resultado:
cd mi-proyecto/src && tsc
cd mi-proyecto && tsc -p src/tsconfig.json
Ver solución
bash
cd 02-Ejercicios/SitioWeb
tsc -p src/tsconfig.json

Terminal real:

zsh · SitioWeb
$ tsc -p src/tsconfig.json
$ ls public/scripts
main.js

Ejercicio 5 — Forzar el 404 de olvidar actualizar el HTML

Cambia <script src="./scripts/main.js"> de vuelta a <script src="main.js"> (como estaba en la Clase 12) SIN mover el .js de lugar. Abre el HTML y revisa qué pasa.

💡 ¿Sabías que…? — el navegador busca rutas relativas al HTML, no a `tsc`

El <script src="..."> es una ruta relativa al archivo HTML que lo contiene — no sabe nada de tu tsconfig.json ni de outDir. Si el .js real vive en una subcarpeta, la ruta en el HTML tiene que reflejarlo exactamente, o el navegador busca en el lugar equivocado y falla en silencio (sin romper la página, solo sin cargar el script).

Ver solución
html
<!-- index.html, a propósito con la ruta vieja -->
<script src="main.js"></script>

Verificado con la red del navegador:

Network
GET http://localhost/main.js
404 Not Found

💡 El archivo real está en scripts/main.js. La solución es restaurar el src del <script> a ./scripts/main.js.

Ejercicio 6 — Subcarpeta dentro de src/ y cómo la refleja outDir

Crea src/utilidades/saludo.ts con export function saludar(nombre: string): string. Impórtala en main.ts y recompila. ¿En qué carpeta aparece saludo.js dentro de public/?

💡 ¿Sabías que…? — `outDir` clona la forma de `src/`, no la aplana

Si organizas src/ con subcarpetas, tsc no mete todo revuelto en outDir — recrea la misma jerarquía de carpetas ahí dentro. Es la misma lógica de la compilación en cascada que ya viste (Clase 8, Clase 11): un archivo que importa a otro arrastra su compilación, y ahora también su ubicación relativa. Ejemplo de referencia:

src/helpers/fecha.ts   →   public/scripts/helpers/fecha.js
Ver solución
typescript
// src/utilidades/saludo.ts
export function saludar(nombre: string): string {
  return `Hola, ${nombre}`;
}
typescript
// src/main.ts
import { saludar } from "./utilidades/saludo.js";
console.log(saludar("Styp"));
bash
tsc
find ../public -type f

Terminal real:

zsh · SitioWeb/src
$ tsc
$ find ../public -type f
../public/index.html
../public/scripts/main.js
../public/scripts/utilidades/saludo.js

💡 utilidades/ apareció dentro de scripts/ sin que lo pidieras explícitamente — outDir refleja la estructura completa de src/.

Ejercicio 7 — Correr tsc sin ningún tsconfig.json presente

En una carpeta nueva y vacía, sin tsconfig.json, crea un .ts cualquiera y corre tsc a secas (sin nombre de archivo). Anticipa qué esperas ver antes de probarlo.

💡 ¿Sabías que…? — `tsc` sin config y sin archivo no compila NADA

tsc a secas necesita un tsconfig.json en la carpeta para saber qué compilar. Sin config y sin nombre de archivo, no asume nada por tu cuenta — en vez de eso, imprime su menú de ayuda y no genera ningún .js. Es un fallo silencioso: no hay mensaje de error rojo, solo ausencia de resultado, fácil de pasar por alto la primera vez.

Ver solución
bash
mkdir carpeta-vacia && cd carpeta-vacia
echo 'console.log("hola");' > main.ts
tsc
ls

Terminal real:

zsh · carpeta-vacia
$ tsc
tsc: The TypeScript Compiler - Version 6.0.3 ...  ← imprime ayuda, no compila
$ ls
main.ts  ← nunca apareció main.js

💡 La solución: o corres tsc main.ts (nombrando el archivo, sin config), o primero tsc --init para crear un tsconfig.json y luego tsc a secas.

Ejercicio 8 — Comparar target: "ES6" vs target: "ES5"

Compila el mismo archivo (con una arrow function y un template string) primero con target: "ES6" y luego con target: "ES5". Compara el .js generado.

💡 ¿Sabías que…? — `target` decide qué tan "moderno" es el JS de salida

target le dice a tsc qué tan antiguo debe ser el JavaScript resultante, para que corra en navegadores viejos que no entienden sintaxis moderna. Cuanto más viejo el target, más "traduce" tu código: arrow functions se vuelven function, const/let se vuelven var, template strings se vuelven .concat(). Ejemplo de referencia:

typescript
const doble = (n: number) => n * 2;

compilado a ES5 se vuelve:

javascript
var doble = function (n) { return n * 2; };
Ver solución
typescript
// demo.ts
const nombre = "Styp";
const saludo = (n: string) => `Hola, ${n}`;
console.log(saludo(nombre));

Con target: "ES6":

javascript
"use strict";
const nombre = "Styp";
const saludo = (n) => `Hola, ${n}`;
console.log(saludo(nombre));

Con target: "ES5":

javascript
"use strict";
var nombre = "Styp";
var saludo = function (n) { return "Hola, ".concat(n); };
console.log(saludo(nombre));

⚠️ Verificado con TypeScript 6.0.3: target: "ES5" sigue funcionando, pero el compilador avisa que está deprecado y dejará de existir en TS 7.0 (error TS5107). Para proyectos nuevos, "ES6" (o más moderno) es lo recomendado — prácticamente todos los navegadores actuales lo soportan sin problema.

Ejercicio 9 — module: "CommonJS" no funciona en el navegador con import

Usa el main.ts del Ejercicio 6 (que importa saludar de utilidades/saludo.ts), deja module: "CommonJS" en el tsconfig.json (como está en este proyecto), compílalo, y cárgalo en un <script> normal. Anticipa el error y verifícalo.

💡 ¿Sabías que…? — CommonJS compila a `require()`, no a `import` nativo

module: "CommonJS" es la configuración estándar para proyectos Nodetsc transforma tus import/export en require()/exports, que es la sintaxis de módulos que Node entiende de forma nativa. El problema: el navegador no tiene require() a menos que un bundler (Webpack, Vite, esbuild…) lo simule. Es un gotcha distinto al de la Clase 12: ahí faltaba type="module"; aquí ni con type="module" se arregla, porque el error ya no es "no sé leer import" — es que el código ya no tiene import, tiene require().

Ver solución
typescript
// main.ts compilado con module: "CommonJS"
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const saludo_js_1 = require("./utilidades/saludo.js");
console.log((0, saludo_js_1.saludar)("Styp"));
html
<script src="scripts/main.js" defer></script>

Verificado en Chrome real:

Chrome DevTools · Console
Uncaught ReferenceError: exports is not defined

La solución real (verificada): cambiar module a "ES6" en el tsconfig.json (así tsc deja el import/export tal cual, en formato ES Modules) y agregar type="module" al <script> del HTML — la combinación exacta que sí entiende el navegador:

json
{ "compilerOptions": { "outDir": "../public/scripts", "target": "ES6", "module": "ES6", "strict": true } }
html
<script src="scripts/main.js" type="module" defer></script>
Chrome DevTools · Console
Hola, Styp

⚠️ Este proyecto real usa module: "CommonJS" — funciona porque main.ts no importa nada de otro archivo. En el momento en que agregues un import, hay que decidir: o cambias a module: "ES6" + type="module", o metes un bundler al proyecto (lo normal en apps reales, mencionado también al cierre de la Clase 12).

Ejercicio 10 — Integrador: migrar la Clase 12 a esta estructura

Toma el proyecto plano de la Clase 12 (02-Ejercicios/Web/, todo junto) y reorganízalo con la estructura src/ + public/ de esta clase: mueve main.ts a src/, agrega su tsconfig.json con outDir, compila, y actualiza index.html en public/ para que apunte al .js generado.

💡 ¿Sabías que…? — migrar una estructura es aplicar TODO lo de esta clase junto

No hay ningún concepto nuevo aquí — es exactamente encadenar lo que ya viste: crear src//public/, mover el .ts, escribir el tsconfig.json con outDir apuntando a public/scripts, compilar, y corregir la ruta del <script>. El objetivo del ejercicio es notar que reorganizar un proyecto existente no es distinto a crearlo desde cero con esta forma — solo cambia el punto de partida.

Ver solución
Web/                          →   Web-organizado/
├── index.html                    ├── public/
├── main.ts                       │   ├── index.html
└── main.js                       │   └── scripts/
                                   │       └── main.js
                                   └── src/
                                       ├── main.ts
                                       └── tsconfig.json
json
// src/tsconfig.json
{
  "compilerOptions": {
    "outDir": "../public/scripts",
    "target": "ES6",
    "module": "CommonJS",
    "strict": true
  }
}
html
<!-- public/index.html -->
<script src="./scripts/main.js"></script>
bash
cd src
tsc

Terminal esperada (mismo mensaje de la Clase 12, ahora desde la estructura nueva):

Chrome DevTools · Console
Hola desde mi explorador

❓ Preguntas y respuestas (autoevaluación)

1. ¿Por qué separar src/ de public/ en un proyecto web con TypeScript?

Porque public/ es lo único que se despliega (HTML, CSS, .js compilado) y src/ es código fuente que nadie más necesita ver. Mezclarlos hace confuso saber qué subir al servidor y expone el .ts innecesariamente.

2. ¿Qué hace la opción outDir en tsconfig.json?

Le dice a tsc en qué carpeta debe dejar los archivos .js compilados, en vez de dejarlos al lado de cada .ts.

3. ¿La ruta de outDir es relativa a mi terminal o al tsconfig.json?

Al tsconfig.json. Por eso tsc -p src/tsconfig.json compila igual sin importar desde qué carpeta corras el comando.

4. Si mi carpeta tiene tsconfig.json y corro tsc main.ts (con el nombre), ¿qué pasa?

Error TS5112: TypeScript no mezcla "archivo por nombre" con "configuración presente" — hay que usar tsc a secas o tsc -p tsconfig.json.

5. ¿Cómo compilo usando un tsconfig.json que está en otra carpeta (no la actual)?

Con tsc -p ruta/al/tsconfig.json — funciona desde cualquier directorio.

6. Si src/ tiene subcarpetas, ¿cómo las refleja outDir?

Las reproduce igual dentro de la carpeta de salida — src/helpers/x.ts termina en outDir/helpers/x.js, no todo aplanado en un solo nivel.

7. ¿Qué pasa si corro tsc a secas en una carpeta sin tsconfig.json y sin nombrar ningún archivo?

No compila nada — imprime el menú de ayuda de tsc en silencio, sin marcar error. Fácil de confundir con "no pasó nada".

8. ¿Qué le pasa al JavaScript generado si cambio target de "ES6" a "ES5"?

Se "traduce" a sintaxis más antigua: arrow functions se vuelven function, const/let se vuelven var, template strings se vuelven .concat(). (En TS 6.0.3, además, ES5 como target está deprecado.)

9. ¿Por qué module: "CommonJS" con import/export no funciona en el navegador, ni agregando type="module" al <script>?

Porque tsc ya transformó el import/export en require()/exports (sintaxis CommonJS, pensada para Node) — el navegador no tiene require() de forma nativa. type="module" solo ayuda si el .js sigue teniendo import/export real (con module: "ES6" o similar).

10. Migré un proyecto de "todo junto" (Clase 12) a src//public/ (Clase 13). ¿Qué pasos seguí?

Moví el .ts a src/, creé su tsconfig.json con outDir apuntando a public/scripts, compilé desde src/, y actualicé el <script src> del index.html en public/ para que apunte a la nueva ruta del .js.

📎 Apuntes relacionados

➡️ Siguiente

Utility typesPartial, Pick, Omit, Record… transformar tipos existentes sin reescribirlos.