Apariencia
📙 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
outDirentsconfig.jsonpara quetsccompile hacia otra carpeta, en vez de dejar el.jsal lado del.ts. - La diferencia entre
tsc archivo.ts(ignora eltsconfig.json),tsca secas (lo toma automático si estás parado en esa carpeta) ytsc -p ruta/tsconfig.json(funciona desde cualquier carpeta). - Que
outDirpreserva la estructura de subcarpetas desrc/dentro depublic/. - Un gotcha real y no obvio:
module: "CommonJS"(el que trae mi propio proyecto) compilaimport/exportarequire()/exports— que tampoco corre en el navegador, ni contype="module"(eso solo entiende ES Modules, no CommonJS).
📚 Definiciones clave
| Término | Qué es | Ejemplo de esta clase |
|---|---|---|
src/ | Carpeta con el código fuente (.ts) — no se sube al servidor | src/main.ts |
public/ | Carpeta con lo que sí se despliega (HTML, CSS, .js compilado) | public/index.html |
outDir | Opción de tsconfig.json: a dónde tsc manda el .js compilado | "outDir": "../public/scripts" |
tsc -p archivo | Compila usando ESE tsconfig.json, sin importar desde qué carpeta lo corras | tsc -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 depublic/scripts/a mano — si lo hago, se pierde en el próximotsc.
⚙️ 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.tscompila en el lugar de siempre.outDirsolo aplica cuandotscusa 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.jsontypescript
// 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
tscVerificado en terminal — tsc (sin argumentos) detecta el tsconfig.json de la carpeta automáticamente y respeta su outDir:
$ 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:
$ 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
GenericosSÍ dejaba compilartsc archivo.tssin problema: esa carpeta no teníatsconfig.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:
| Comando | Desde dónde funciona | Qué hace |
|---|---|---|
tsc (sin nada) | Solo si estás parado en la carpeta del tsconfig.json | Lo detecta automático |
tsc -p tsconfig.json | Desde la misma carpeta | Lo carga explícitamente |
tsc -p src/tsconfig.json | Desde 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:
$ 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:
Hola desde mi explorador⚠️ Si olvidas actualizar el
<script src>y lo dejas apuntando amain.js(sin la subcarpetascripts/), el navegador busca el archivo en el lugar equivocado. Verificado:GET http://localhost/main.jsresponde 404 — elmain.jsreal está enscripts/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
tscSalida 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í:
$ find . -type f
./public/index.html
./public/scripts/main.js
./src/main.ts
./src/tsconfig.jsonTodo 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áticamenteVer solución
typescript
// src/main.ts
console.log("Hola, este mensaje lo cambié yo");bash
cd 02-Ejercicios/SitioWeb/src
tscConsola del navegador tras recargar (sirviendo public/index.html):
Hola, este mensaje lo cambié yoEjercicio 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
tscTerminal esperada:
$ 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 reviertasoutDira 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.tsTerminal real:
$ 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) otsc -p tsconfig.json— nunca mezclar nombre de archivo con untsconfig.jsonpresente.
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.jsonVer solución
bash
cd 02-Ejercicios/SitioWeb
tsc -p src/tsconfig.jsonTerminal real:
$ tsc -p src/tsconfig.json
$ ls public/scripts
main.jsEjercicio 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:
GET http://localhost/main.js
404 Not Found💡 El archivo real está en
scripts/main.js. La solución es restaurar elsrcdel<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.jsVer 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 fTerminal real:
$ tsc
$ find ../public -type f
../public/index.html
../public/scripts/main.js
../public/scripts/utilidades/saludo.js💡
utilidades/apareció dentro descripts/sin que lo pidieras explícitamente —outDirrefleja la estructura completa desrc/.
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
lsTerminal real:
$ 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 primerotsc --initpara crear untsconfig.jsony luegotsca 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 Node — tsc 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:
Uncaught ReferenceError: exports is not definedLa 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>Hola, Styp⚠️ Este proyecto real usa
module: "CommonJS"— funciona porquemain.tsno importa nada de otro archivo. En el momento en que agregues unimport, hay que decidir: o cambias amodule: "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.jsonjson
// 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
tscTerminal esperada (mismo mensaje de la Clase 12, ahora desde la estructura nueva):
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,.jscompilado) ysrc/es código fuente que nadie más necesita ver. Mezclarlos hace confuso saber qué subir al servidor y expone el.tsinnecesariamente.
2. ¿Qué hace la opción outDir en tsconfig.json?
Le dice a
tscen qué carpeta debe dejar los archivos.jscompilados, 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 esotsc -p src/tsconfig.jsoncompila 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 usartsca secas otsc -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.tstermina enoutDir/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
tscen 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/letse vuelvenvar, template strings se vuelven.concat(). (En TS 6.0.3, además,ES5como target está deprecado.)
9. ¿Por qué module: "CommonJS" con import/export no funciona en el navegador, ni agregando type="module" al <script>?
Porque
tscya transformó elimport/exportenrequire()/exports(sintaxis CommonJS, pensada para Node) — el navegador no tienerequire()de forma nativa.type="module"solo ayuda si el.jssigue teniendoimport/exportreal (conmodule: "ES6"o similar).
10. Migré un proyecto de "todo junto" (Clase 12) a src//public/ (Clase 13). ¿Qué pasos seguí?
Moví el
.tsasrc/, creé sutsconfig.jsonconoutDirapuntando apublic/scripts, compilé desdesrc/, y actualicé el<script src>delindex.htmlenpublic/para que apunte a la nueva ruta del.js.
📎 Apuntes relacionados
- Clase 8: compilación en cascada
- Clase 9: el error
TS5112la primera vez que apareció - Clase 11:
export/importentre archivos - Clase 12: TS en el navegador,
<script>,type="module" - Conceptos
- Comandos
➡️ Siguiente
Utility types — Partial, Pick, Omit, Record… transformar tipos existentes sin reescribirlos.