Repository navigation
study guide
Guía para aprender TypeScript y Angular desde cero, siguiendo los proyectos del repositorio. No reemplaza la documentación oficial: explica qué problema resuelve cada concepto, cómo funciona por dentro y cómo se ve en el código del curso.
Regla de lectura: primero ejecuta el ejemplo del repositorio, después lee la teoría del módulo y al final intenta responder las preguntas de entrevista sin mirar la respuesta.
- Cada proyecto del repositorio (
01-typescript-intro/, …) es una parte de esta guía. - Cada archivo de
src/topics/es un módulo. El número del módulo coincide con el número del archivo. - Cada módulo termina con preguntas de entrevista de nivel senior, cada una con su respuesta debajo. Intenta responder antes de leerla.
- Si algo no te queda claro, cámbialo en el archivo del tópico, ejecuta
npx tscy mira qué error aparece. El compilador es el mejor profesor.
angular-ufh/
└── 01-typescript-intro/ Parte 1: TypeScript sin framework
└── src/topics/
├── 01-basic-types.ts Módulo 1: tipos básicos
├── 02-object-interface.ts Módulo 2: arrays e interfaces
├── 03-functions.ts Módulo 3: funciones
├── 04-homework-types.ts Módulo 4: tarea, interfaces anidadas
└── 05-basic-destructuring.ts Módulo 5: desestructuración
| Etapa | Tema | Resultado esperado |
|---|---|---|
| 0 | Qué es TypeScript y cómo corre el proyecto | Saber qué hace tsc y qué hace Vite |
| 1 | Tipos básicos, literales y uniones | Leer y escribir anotaciones de tipo sin dudar |
| 2 | Arrays tipados e interfaces | Describir la forma de un objeto y entender los errores |
| 3 | Funciones y parámetros | Tipar parámetros y retornos, opcionales y por defecto |
| 4 | Tarea: interfaces anidadas | Componer interfaces y nombrar tipos según convención |
| 5 | Desestructuración de objetos | Extraer, renombrar y anidar valores sin errores |
Angular es un framework opinionado: ya decidió por ti cómo se enruta, cómo se hacen los formularios, cómo se pide información a una API y cómo se organiza el código. Eso promueve orden y legibilidad, porque dos proyectos de Angular se parecen mucho entre sí.
Con los años se volvió más flexible. Antes todo componente tenía que
declararse dentro de un módulo (NgModule); hoy los componentes son
standalone: cada uno declara lo que necesita y los módulos son opcionales.
Un decorador es una etiqueta que empieza con @ y se pone encima de una
clase (o de una propiedad) para decirle a Angular qué es y cómo usarla. La
clase sola es TypeScript normal; el decorador es lo que la convierte en
componente, servicio o directiva.
@Component({
selector: 'app-hero',
template: `<h1>{{ name }}</h1>`
})
export class Hero {
name = 'Geralt'
}Sin @Component, Hero es una clase cualquiera. Con él, Angular sabe que es
una pieza de pantalla, con qué etiqueta HTML se usa (<app-hero>) y qué
dibuja.
| Decorador | Qué le dice a Angular |
|---|---|
@Component |
Es una pieza de pantalla, con su template |
@Directive |
Agrega comportamiento a un elemento que ya existe |
@Pipe |
Transforma un valor para mostrarlo ({{ price | currency }}) |
@Injectable |
Es un servicio: Angular puede crearlo y entregarlo a quien lo pida |
@Input / @Output
|
Datos que entran al componente / eventos que salen |
Por qué ayudan a leer el código: abres un archivo y, con sólo ver el decorador, ya sabes qué papel cumple esa clase.
Por dentro, un decorador es una función que recibe la clase y le adjunta información (metadata). Angular lee esa información al compilar.
Ojo: en el Angular actual varios decoradores de propiedad tienen un reemplazo
en forma de función (input() y output() en lugar de @Input y @Output).
Los de clase (@Component, @Injectable, @Directive, @Pipe) se siguen
usando igual.
Antes de empezar conviene despejar algunas ideas que se repiten mucho y que no son ciertas, o que ya dejaron de serlo.
Realidad: no, son herramientas distintas. Angular es un framework opinionado: ya trae resuelto el enrutado, los formularios y las peticiones HTTP, y te dice cómo organizar el código. React es una librería, y Vue, Svelte y Solid son frameworks más livianos: dan más libertad, pero dejan más decisiones en manos del equipo. Ninguno es mejor en general; depende de lo que necesite tu proyecto.
Ojo: la herramienta no garantiza el orden. La estructura de carpetas y la disciplina dependen del equipo, y también hay proyectos de Angular desordenados.
Realidad: no. Angular publica una versión mayor cada seis meses, pero la
mayoría de los cambios no son disruptivos. Y cuando lo son, no hay que
aprender de nuevo todo el framework: lo que ya sabes sigue sirviendo. Además,
ng update hace buena parte de la migración por ti, la documentación oficial
es muy buena y la comunidad es grande, así que ponerse al día cuesta poco.
Realidad: cada versión reduce el bundle (lo que el navegador tiene que descargar). Hoy el peso depende sobre todo de ti y de las buenas prácticas que apliques.
Para que pese menos:
-
Lazy loading de rutas. Cada pantalla se descarga sólo cuando alguien
la visita, en lugar de bajar todo al entrar:
{ path: 'heroes', loadComponent: () => import('./heroes/heroes').then(m => m.Heroes) }. -
@deferen el template. Lo mismo, pero para un pedazo de pantalla: un gráfico pesado se descarga recién cuando aparece en el área visible. - Cuidar las dependencias. Cada librería que instalas viaja al navegador. Importa sólo lo que usas y desconfía de las librerías enormes para tareas pequeñas.
- Componentes compartidos. Un botón o una tarjeta se escriben una vez y se reutilizan; copiar y pegar el mismo código en diez pantallas lo descarga diez veces.
-
Budgets. En
angular.jsonse define un tamaño máximo; si el build lo supera, avisa o falla. Así el peso no crece sin que nadie lo note.
Para que no se vuelva lenta con el uso (no cambia el peso, sí la experiencia):
-
Cerrar las suscripciones. Una suscripción abierta sigue trabajando
aunque el componente ya no esté en pantalla, y la memoria nunca se libera.
Se evita con el pipe
asyncen el template o contakeUntilDestroyed(). -
Respetar el ciclo de vida. Lo que el componente abre (un temporizador,
un listener) lo cierra al destruirse (
ngOnDestroyoDestroyRef). -
Signals u
OnPush. Angular revisa sólo los componentes cuyos datos cambiaron, en lugar de revisar todos. -
tracken@for. Al cambiar una lista, Angular reutiliza los elementos que ya estaban en pantalla en lugar de dibujarlos de nuevo.
Realidad: sí lo es. Angular puede armar la página en el servidor (SSR) o dejarla armada de antemano al compilar (prerender). En los dos casos Google recibe HTML con el contenido ya puesto, no una página vacía que depende de JavaScript. Después, en el navegador, Angular «hidrata» esa página: la vuelve interactiva sin dibujarla otra vez.
De dónde viene el mito. Una aplicación clásica de Angular se dibuja en el
navegador (CSR: client-side rendering, «renderizado del lado del cliente»). El servidor manda un HTML casi
vacío, apenas <app-root></app-root> y los scripts, y el contenido aparece
recién cuando el JavaScript se descarga y corre. Un robot que lee ese HTML sin
ejecutar JavaScript no ve nada. Google sí ejecuta JavaScript, pero lo hace más
tarde y con límites; las vistas previas de redes sociales y apps de mensajería
directamente no lo ejecutan.
Las tres formas de dibujar la página:
CSR servidor manda HTML vacío ------> el navegador baja el JS -> el JS dibuja la página
SSR servidor dibuja en cada visita -> manda HTML completo ----> hidratación
Prerender se dibuja una vez al compilar --> se guarda como .html ---> hidratación
- SSR (server-side rendering, «renderizado del lado del servidor»): sirve cuando el contenido cambia seguido o depende de quién visita (una tienda, un perfil público).
- Prerender (también llamado SSG): sirve cuando el contenido casi no cambia (un blog, una página de presentación). Es lo más rápido, porque el HTML ya está hecho.
- CSR: alcanza para pantallas detrás de un login, donde el SEO no importa.
Qué es la hidratación. El HTML que llega del servidor se ve, pero todavía no responde: los botones no hacen nada. Hidratar es el momento en que Angular, ya en el navegador, reconoce ese HTML que está en pantalla y le conecta los eventos y el estado. No lo borra ni lo dibuja otra vez; antes de la versión 16 sí lo hacía, y la página parpadeaba.
Un beneficio extra. Como el contenido llega ya dibujado, la página se ve antes. Google también mide esa velocidad para posicionar.
Ojo: hay que activarlo (ng new --ssr al crear el proyecto, o
ng add @angular/ssr después). Sin eso, la aplicación se dibuja sólo en el
navegador y el HTML inicial llega casi vacío.
Realidad: sí lo soporta. Redux es una forma de guardar todo el estado de la aplicación en un solo lugar, y en Angular se usa con librerías como NgRx o NGXS. Además, hoy Angular trae Signals, una opción nativa y más simple para manejar estado, que alcanza para muchas aplicaciones.
Realidad: también puedes usar librerías y código JavaScript, porque al final TypeScript se convierte en JavaScript para correr en el navegador.
- Si una librería JavaScript no trae tipos, se instalan aparte con
@types/nombre-libreria. Hoy la mayoría ya los incluye. - Puedes escribir archivos
.jspropios activandoallowJsentsconfig.json.
Es posible, pero se recomienda TypeScript: sin tipos pierdes el aviso de errores antes de ejecutar (ver la Introducción de la Parte 1).
Angular no es mejor ni peor que las demás opciones, no hay que reaprenderlo cada seis meses, no es pesado por obligación, sí sirve para SEO, sí soporta Redux y sí acepta JavaScript. Casi todos estos mitos vienen de versiones antiguas del framework.
Proyecto: 01-typescript-intro/
JavaScript no revisa tipos hasta que el código se ejecuta. Un error como
llamar .toUpperCase() sobre un número sólo aparece cuando esa línea corre,
quizás en producción.
// JavaScript: no avisa nada al escribirlo
const hp = 39
hp.toUpperCase() // TypeError en runtimeTypeScript agrega un sistema de tipos que se revisa antes de ejecutar. El mismo error aparece en el editor, mientras escribes. En esta guía, runtime significa «cuando el código ya se está ejecutando».
TypeScript es un superset de JavaScript: es JavaScript con tipos encima.
Casi todo el JavaScript que ya conoces también es TypeScript válido. Los tipos
sirven para dos cosas: saber si el código tiene errores antes de
ejecutarlo, y que se lea mejor (con ver hp: number ya sabes qué guarda esa
variable).
archivo.ts
|
+--> tsc ............ revisa tipos, informa errores (no genera archivos: noEmit)
|
+--> Vite ........... quita los tipos y entrega JavaScript al navegador
|
v
el navegador ejecuta JavaScript
Idea clave: los tipos se borran al compilar (type erasure). Son como las
notas a lápiz en un borrador: ayudan mientras escribes, pero no salen en la
versión final. En el navegador no queda ningún string, interface ni
type. Por eso TypeScript no puede revisar los datos que llegan cuando la
aplicación ya está corriendo (la respuesta de una API, lo que alguien escribe
en un formulario). Para eso hace falta código de verdad: una función que
compruebe el dato (type guard) o una librería de validación.
Los navegadores sólo entienden JavaScript, y los más viejos sólo entienden JavaScript viejo. Un transpilador (o transformador) es un programa que traduce código a otro código equivalente: de JavaScript moderno a uno más antiguo, o de TypeScript a JavaScript.
Babel es el transpilador más conocido. Nació para poder escribir JavaScript moderno y que igual funcionara en navegadores viejos. Se arma con plugins y presets: uno por cada cosa que quieres traducir, y hay que configurarlos a mano.
JavaScript moderno --Babel--> JavaScript que entiende un navegador viejo
TypeScript --tsc---> JavaScript (la versión que pidas en "target")
Con TypeScript ese paso extra no hace falta: el compilador ya sabe traducir la
sintaxis moderna a la versión de JavaScript que le indiques en target.
Escribes con las características nuevas del lenguaje y sale código compatible,
sin instalar ni configurar Babel.
Ojo: traducir sintaxis no es lo mismo que agregar funciones que el navegador
no tiene. Si usas un método nuevo (por ejemplo array.at(-1)) en un navegador
que no lo trae, ningún transpilador lo inventa; para eso existen los
polyfills.
En este repositorio quien traduce es Vite, con su transpilador integrado
(tsc sólo revisa). En Angular lo hace el CLI: tampoco configuras nada.
-
npm run devlevanta Vite. Vite no revisa tipos: si hay un error de tipos, la página carga igual. -
npx tsces quien revisa tipos. Ejecútalo seguido. - TypeScript 6.0 activa el modo
strictpor defecto, aunquetsconfig.jsonno lo diga. Eso incluyenoImplicitAnyystrictNullChecks. -
noUnusedLocalsestá activo: una variable de ejemplo que no se usa rompe el build. Por eso cada tópico termina conexport { ... }.
Angular está escrito en TypeScript y usa el mismo compilador, al que le suma el suyo propio para revisar también los templates HTML. Todo lo que aprendas aquí lo vas a usar tal cual en componentes, servicios y formularios.
- Creer que si la página carga, el código no tiene errores de tipos. Vite no
revisa tipos: usa
npx tsc. - Creer que una anotación valida datos en runtime.
const user: User = await res.json()no comprueba nada; sólo le dice al compilador que confíe.
TypeScript revisa antes de ejecutar y desaparece al ejecutar. tsc revisa,
Vite compila.
1. Si los tipos se borran en runtime, ¿cómo garantizas que la respuesta de una API cumple una interfaz?
Respuesta:
No se puede con tipos solos. Hay que comprobar el dato cuando llega, justo en
la entrada de la aplicación: con un type guard escrito a mano
(function isUser(x: unknown): x is User) o con una librería de esquemas
(Zod, Valibot) que genere el tipo a partir del esquema, así el tipo y la
validación nunca dicen cosas distintas. Una vez comprobado en la entrada, el
resto de la aplicación puede confiar en el tipo.
Respuesta:
Transpilar es quitar la sintaxis de TypeScript y producir JavaScript; se puede
hacer archivo por archivo, muy rápido (esbuild, SWC, Oxc). Revisar tipos
requiere entender el programa completo, porque un tipo puede venir de otro
archivo. Vite separa las dos tareas para que el servidor de desarrollo sea
rápido; la revisión se hace con tsc (en el editor, en CI o en npm run build).
Respuesta:
Agrupa varias revisiones, entre ellas noImplicitAny (prohíbe parámetros sin
tipo que caen en any), strictNullChecks (null y undefined dejan de
ser asignables a cualquier tipo) y strictPropertyInitialization. En un
proyecto nuevo cuesta poco activarlo; en uno existente se activa de a una
opción para no tener miles de errores juntos. Angular genera proyectos con
strict activo.
Respuesta:
Las dos existen por la misma razón: las herramientas que convierten archivo
por archivo (como Vite) no ven los demás archivos, así que no pueden saber si
un import trae un tipo o un valor real. isolatedModules hace que tsc avise
cuando escribes algo que esas herramientas no pueden convertir mirando un solo
archivo. verbatimModuleSyntax va un paso más allá: obliga a escribir
import type { X } para los tipos, y así la herramienta puede borrar ese
import sin mirar nada más. Este repositorio tiene activo
verbatimModuleSyntax.
Archivo: 01-typescript-intro/src/topics/01-basic-types.ts
Una variable que guarda la vida de un personaje debería aceptar números y
quizás algunos estados fijos ('FULL', 'DEAD'), pero no cualquier texto.
Sin tipos, hpPoints = 'FUL' (con un error de tipeo) pasaría sin aviso.
-
Anotación de tipo: decirle al compilador qué tipo tiene algo
(
const name: string). -
Tipos primitivos:
string,number,boolean,bigint,symbol,null,undefined. -
Tipo literal: un tipo que acepta un único valor exacto (
'FULL'y nada más). -
Unión (
|): se lee «o».number | stringes «un número o un texto». -
Alias de tipo (
type): ponerle nombre a un tipo para no repetirlo.
const name: string = 'Titus'
const isAlive: boolean = true
type LifeStatus = 'FULL' | 'DEAD' | 'INJURED'
let hpPoints: number | LifeStatus = 39
hpPoints = 'FULL'-
LifeStatuses una unión de tres tipos literales. Sólo esos tres textos son válidos. -
hpPointsacepta cualquiernumbero uno de esos tres textos. -
hpPoints = 'FUL'daría error entsc.
Los comentarios del archivo plantean tres preguntas de diseño.
1. ¿Unión o tipo literal? No son opciones distintas: se combinan. Cada
texto ('FULL') es un tipo literal, y | los une:
'FULL' tipo literal: acepta un solo valor
'FULL' | 'DEAD' | 'INJURED' unión de tres tipos literales -> LifeStatus
number | LifeStatus unión de number con esa unión -> tipo de hpPoints
Un tipo literal solo (let hp: 'FULL') sería demasiado estricto: la variable
sólo podría valer 'FULL'. La unión es lo que permite elegir entre varios
valores.
2. ¿Por qué no un enum? La razón más concreta se ve en el propio
archivo. Con un enum de textos, la línea hpPoints = 'FULL' deja de
compilar:
enum LifeStatusEnum { FULL = 'FULL', DEAD = 'DEAD', INJURED = 'INJURED' }
let hpPoints: number | LifeStatusEnum = 39
hpPoints = 'FULL' // error: Type '"FULL"' is not assignable to type 'number | LifeStatusEnum'.
hpPoints = LifeStatusEnum.FULL // así síUn enum de textos sólo acepta sus propios miembros, aunque el texto sea
idéntico. Con la unión, el valor es un string común y corriente. Otras
diferencias: la unión no genera código en runtime (desaparece al compilar) y
el enum sí genera un objeto. La comparación completa está en la pregunta 4.
3. ¿Por qué type y no interface? Porque LifeStatus no describe un
objeto. Una interface sólo puede describir formas de objetos (con sus
propiedades y métodos); no puede nombrar una unión ni un primitivo. type
puede nombrar cualquier tipo:
type LifeStatus = 'FULL' | 'DEAD' | 'INJURED' // unión: sólo con type
type Hp = number // primitivo: sólo con type
interface Character { name: string; hp: Hp } // objeto: interface o typePara objetos, la elección entre interface y type es en gran parte de
estilo, pero hay diferencias reales; están en la tabla del Módulo 2.
Inferencia. Si no anotas, TypeScript deduce el tipo del valor inicial:
const a = 'Titus' // tipo: 'Titus' (literal: un const no puede cambiar)
let b = 'Titus' // tipo: string (se "ensancha": un let puede cambiar)A esto se le llama widening (ensanchar): pasar de un valor exacto a un tipo
más amplio. En el archivo, const name: string es string y no 'Titus'
porque la anotación lo ensancha a propósito.
Narrowing (estrechamiento). Es lo contrario: ir descartando opciones hasta
saber qué hay. El tipo declarado de hpPoints es
number | LifeStatus, pero después de hpPoints = 'FULL' el compilador sabe
que en ese punto vale exactamente 'FULL'. TypeScript sigue el flujo del
código y estrecha el tipo:
function describe(hp: number | LifeStatus) {
if (typeof hp === 'number') {
return hp.toFixed(0) // aquí hp es number
}
return hp.toLowerCase() // aquí hp es LifeStatus
}any, unknown y never:
unknown "puede ser cualquier cosa; demuéstrame qué es antes de usarlo" (seguro)
any "puede ser cualquier cosa; no reviso nada" (apaga el sistema)
never "esto no puede ocurrir" (ningún valor entra aquí)
typeof existe en JavaScript y devuelve un texto en runtime. TypeScript lo
reutiliza para estrechar tipos dentro de un if. Lo que TypeScript agrega
(el tipo LifeStatus) desaparece al compilar; el typeof queda.
En el navegador ya existe una variable global name (window.name). Si este
archivo no fuera un módulo, const name chocaría con ella.
moduleDetection: "force" hace que cada archivo sea un módulo con su propio
alcance, y por eso no hay conflicto.
- Usar
anypara "salir del paso". Usaunknowny estrecha. - Anotar todo. Si el valor inicial deja claro el tipo, la inferencia basta; anota en los bordes (parámetros, retornos públicos, datos externos).
- Creer que
numberdistingue enteros de decimales. Es un único tipo de punto flotante (0.1 + 0.2 !== 0.3). Para enteros grandes existebigint.
Anota lo necesario, deja que la inferencia haga el resto, y usa uniones de
literales para limitar los valores posibles. Para nombrar una unión se usa
type; interface es sólo para objetos.
Respuesta:
any desactiva la revisión: se puede asignar a todo y usar como sea; se
contagia a lo que toca. unknown acepta cualquier valor pero no deja usarlo
hasta estrecharlo (typeof, instanceof, un type guard); es el tipo
correcto para datos externos y para catch (e). never es el tipo donde no
entra ningún valor: lo que devuelve una función que siempre lanza un error, o
el caso «imposible» de un switch que ya cubrió todas las opciones
(pregunta 3). any sólo se justifica en migraciones o con librerías
mal tipadas, y aislado.
Respuesta:
const infiere el literal 'a' porque el valor no puede cambiar. let
infiere string (widening) porque se espera reasignarlo. Lo mismo pasa con
las propiedades de un objeto: { status: 'FULL' } infiere status: string,
salvo que se use as const o se anote el tipo.
Respuesta:
Con un chequeo exhaustivo usando never:
function label(s: LifeStatus): string {
switch (s) {
case 'FULL': return 'Sano'
case 'DEAD': return 'Muerto'
case 'INJURED': return 'Herido'
default: {
const unreachable: never = s
return unreachable
}
}
}Si mañana se agrega 'POISONED' a LifeStatus, s deja de ser never en el
default y el compilador marca el error en todos los switch que faltan.
Respuesta:
- Unión de literales: cero código en runtime, se lee bien, buen autocompletado. No da una lista de valores para iterar.
-
enum: genera un objeto real en runtime (se puede iterar), pero es sintaxis propia de TypeScript que no se puede borrar sin más; herramientas comoerasableSyntaxOnlyo el modo de "sólo quitar tipos" de Node la rechazan. Unenumde textos no acepta el texto literal ('FULL'), sóloLifeStatusEnum.FULL, lo que complica recibir datos de una API. Unenumnumérico rechaza un literal fuera de rango (const e: E = 42da error desde TypeScript 5.0), pero sigue aceptando cualquier variable de tiponumber. -
const STATUS = { FULL: 'FULL', ... } as const+type Status = typeof STATUS[keyof typeof STATUS]: da valores en runtime y un tipo derivado, sin sintaxis especial.
En código nuevo suele preferirse la unión o el objeto as const; enum sigue
siendo común en código Angular existente. En este repositorio enum está
permitido a propósito.
Respuesta:
Sin él, null y undefined se pueden asignar a cualquier tipo, y
user.name.length compila aunque user pueda ser null. Con él, hay que
declararlo (string | null) y estrecharlo antes de usarlo (if (user),
user?.name, ??). Es la opción que más errores reales evita.
Respuesta:
No. Una interface sólo describe objetos, y ni siquiera puede extender una
unión: interface X extends LifeStatus {} da
An interface can only extend an object type or intersection of object types with statically known members.
Sólo type puede nombrar:
- Uniones (
'FULL' | 'DEAD') y primitivos (type Hp = number). - Tuplas (
type Point = [number, number]). - Tipos mapeados (
type ReadonlyCharacter = { readonly [K in keyof Character]: Character[K] }). - Tipos condicionales (
type IsString<T> = T extends string ? true : false). - Tipos derivados de valores (
type Status = typeof STATUS[keyof typeof STATUS]).
A cambio, sólo interface permite la fusión de declaraciones (declaration
merging). Por eso una regla práctica común es: interface para formas de
objetos que otros pueden extender, type para todo lo demás.
Archivo: 01-typescript-intro/src/topics/02-object-interface.ts
Un personaje tiene varias propiedades (nombre, vida, habilidades). Sin una
descripción de esa forma, nada impide escribir geralt.hp = 'cien', olvidar
hometown o equivocarse en el nombre de una propiedad.
-
Array tipado:
string[]es una lista donde todos los elementos sonstring. -
Interfaz (
interface): describe la forma de un objeto, es decir, qué propiedades tiene y de qué tipo. -
Propiedad opcional (
?): puede estar o no estar.
const skills: string[] = ['Bash', 'Counter', 'Healing', 'Magic', 'Stealth']
interface Character {
name: string
hp: number
skills: string[]
hometown: string
age?: number
}
const geralt: Character = {
name: 'Geralt of Rivia',
hp: 100,
skills: ['Bash', 'Counter'],
hometown: 'Rivia'
}
geralt.age = 100- Sin la anotación,
['Bash', true, 42]se infiere como(string | number | boolean)[]. La anotaciónstring[]obliga a que todos sean texto. -
geraltno tieneageal crearse, y es válido porqueagees opcional. -
geralt.age = 100funciona aunquegeraltseaconst:constimpide reasignar la variable, no modificar el objeto.
Tipado estructural. TypeScript compara formas, no nombres. Es la regla de
«si camina como pato y hace cuac, es un pato»: cualquier objeto con las
propiedades de Character cuenta como un Character, aunque nunca se haya
declarado así.
Nominal (Java, C#) Estructural (TypeScript)
"¿se declaró Character?" "¿tiene name, hp, skills, hometown?"
Revisión de propiedades sobrantes. Si escribes un objeto literal
directamente donde se espera un Character, TypeScript rechaza propiedades
que no existen en la interfaz:
const c: Character = { ..., extra: 1 } // error: 'extra' no existe en Character
const raw = { ..., extra: 1 }
const c2: Character = raw // sin error: raw ya no es un literal "fresco"La revisión sólo aplica a objetos escritos ahí mismo, entre llaves (lo que se llama un literal «fresco»), porque en ese caso una propiedad de más casi siempre es un error de tipeo. Con una variable intermedia, el tipado estructural acepta el objeto porque tiene todo lo que pide la interfaz.
Opcional no es lo mismo que undefined:
interface A { age?: number } // la propiedad puede faltar
interface B { age: number | undefined } // la propiedad debe existir, aunque valga undefinedEn Angular las interfaces se usan para describir los datos: la respuesta de
una API (http.get<Character[]>(...)), las entradas de un componente
(@Input() character!: Character o input<Character>()) y los modelos de
formularios. Recuerda que en http.get<Character[]> la interfaz no valida
nada en runtime (ver la Introducción).
interface Character { name: string }
type Character2 = { name: string }Para objetos, las dos sirven casi igual. Diferencias que importan:
interface |
type |
|---|---|
| Se puede declarar dos veces y se fusiona | Un nombre, una declaración |
Se extiende con extends
|
Se combina con & (intersección) |
| Sólo describe objetos (y funciones, clases) | Describe cualquier tipo: uniones, tuplas, etc. |
- Pensar que
consthace el objeto inmutable. Para eso existereadonly(readonly name: string,readonly string[]) oObject.freezeen runtime. - Usar
any[]porque "el array tiene de todo". Si los elementos son de tipos distintos con posiciones fijas, es una tupla:[string, number]. - Dejar todo opcional para evitar errores. Cada
?obliga a revisarundefineden todos los lugares donde se usa.
Las interfaces describen formas; TypeScript compara formas, no nombres. Los literales escritos directamente reciben una revisión extra de propiedades.
Respuesta:
type es obligatorio para uniones, tuplas, tipos mapeados y condicionales.
Para formas de objetos ambos sirven; muchas guías prefieren interface porque
los errores son más legibles, extends falla temprano cuando hay propiedades
incompatibles (una intersección & puede producir never en silencio) y el
compilador trabaja más rápido con interfaces en proyectos grandes. interface además
permite declaration merging, útil para extender tipos de librerías
(por ejemplo, agregar propiedades a Window). Lo importante es ser
consistente en el proyecto.
2. ¿Por qué const c: Character = { ..., extra: 1 } falla pero pasando por una variable intermedia no?
Respuesta:
Por la revisión de propiedades sobrantes (excess property checking), que
sólo aplica a literales de objeto "frescos" asignados directamente a un tipo.
Con una variable intermedia rige el tipado estructural normal: el objeto tiene
todo lo que Character exige, así que es asignable. Es una ayuda para
detectar errores de tipeo, no una garantía de que el objeto no tenga
propiedades extra; al ejecutarse puede tenerlas.
Respuesta:
Dos tipos son compatibles si sus formas lo son, sin importar el nombre. Esto
causa problemas cuando dos conceptos tienen la misma forma (UserId y
OrderId, ambos string). Se simula tipado nominal con branded types, que
consisten en pegarle al tipo una etiqueta que sólo existe para el compilador:
type UserId = string & { readonly __brand: 'UserId' }
const toUserId = (s: string) => s as UserIdAsí un OrderId no se puede pasar donde se espera un UserId.
Respuesta:
Con ? la propiedad puede faltar del todo; con | undefined debe estar
presente, aunque sea con valor undefined. Por defecto TypeScript también
permite asignar undefined explícito a una propiedad ?; con
exactOptionalPropertyTypes eso se prohíbe, y la diferencia importa para
'age' in obj, Object.keys o al serializar a JSON.
Respuesta:
const impide reasignar la variable, no cambiar el objeto. readonly (y
ReadonlyArray<T> / readonly T[]) impide modificar en tiempo de compilación,
pero desaparece en runtime y es superficial (las propiedades anidadas siguen
siendo mutables). Object.freeze protege en runtime y también es superficial.
En Angular, tratar los datos como inmutables importa para la detección de
cambios con OnPush y con signals: si mutas un objeto en lugar de crear uno
nuevo, la referencia no cambia y la vista puede no actualizarse.
Archivo: 01-typescript-intro/src/topics/03-functions.ts
Una función es un contrato: recibe ciertos datos y devuelve un resultado. En
JavaScript nada impide llamar addNumbers('5', 10) (devuelve '510', un
texto) u olvidar un argumento (el resultado es NaN). TypeScript revisa ese
contrato en cada llamada.
-
Tipo de parámetro: qué recibe la función (
a: number). -
Tipo de retorno: qué devuelve (
): number). -
Parámetro opcional (
b?: number): se puede omitir; si se omite valeundefined. -
Parámetro por defecto (
base: number = 2): si se omite, toma ese valor. -
void: la función no devuelve nada útil.
function addNumbers(a: number, b: number): number {
return a + b
}
const result = addNumbers(5, 10)
console.log({ result }) // { result: 15 }Parámetros sin tipo. TypeScript no deduce el tipo de un parámetro a
partir de cómo se llama la función, así que function addNumbers(a, b) {}
deja a y b como any implícito. En este proyecto eso es un error, no
sólo una mala práctica: strict incluye noImplicitAny.
error TS7006: Parameter 'a' implicitly has an 'any' type.
void no es undefined. Una función sin return, como
function addNumbers(a, b) {}, tiene retorno void. En runtime devuelve
undefined, pero para el compilador void significa "no uses este
resultado", y no se puede asignar a una variable de tipo undefined:
const noop = () => {}
const r: undefined = noop() // error: Type 'void' is not assignable to type 'undefined'console.log({ result }). No es TypeScript, es una abreviatura de
JavaScript moderno (ES2015): { result } equivale a { result: result }. Se
usa para que la consola muestre el nombre de la variable junto a su valor.
const addNumbersArrow = (a: number, b: number): number => a + b
const addNumbersArrowToString = (a: number, b: number): string => {
return `The result is: ${a + b}`
}- Con una sola expresión, el
returnes implícito (=> a + b). Con llaves hay que escribirreturn. - Las comillas invertidas son un template literal:
${a + b}inserta el resultado dentro del texto. - La diferencia importante con
functionesthis: una función flecha no tiene su propiothis, usa el del lugar donde se escribió. Ver la pregunta 1.
Si una función declara tres parámetros, hay que pasar los tres:
function multiply(firstNumber: number, secondNumber: number, base: number): number {
return firstNumber * secondNumber * base
}
multiply(2) // error TS2554: Expected 3 arguments, but got 1.La versión del repositorio hace opcional el segundo y le da un valor por defecto al tercero:
function multiply(firstNumber: number, secondNumber?: number, base: number = 2): number {
return firstNumber * (secondNumber ?? 1) * base
}
multiply(2) // 2 * 1 * 2 = 4parámetro si se omite dentro de la función
firstNumber error number
secondNumber? undefined number | undefined -> por eso el ?? 1
base = 2 toma 2 number
-
??(nullish coalescing) usa el valor de la derecha sólo si el de la izquierda esnulloundefined. -
Un parámetro opcional no puede ir antes de uno obligatorio:
function f(a?: number, b: number)daerror TS1016: A required parameter cannot follow an optional parameter. -
Un parámetro por defecto sí puede ir antes de uno obligatorio, pero
entonces deja de ser omitible: hay que pasar
undefineda mano para usar el valor por defecto (f(undefined, 2)). Por eso se ponen al final.
Una interfaz puede describir también las funciones que tiene un objeto:
interface Character {
name: string
hp: number
showHp: () => void
}
const geralt: Character = {
name: 'Geralt',
hp: 70,
showHp() {
console.log(`Current HP of ${this.name} is: ${this.hp}`)
}
}-
showHp: () => voiddice: "este objeto tiene una propiedadshowHpque es una función sin parámetros y cuyo resultado no se usa". - Todo objeto declarado como
Characterestá obligado a tenershowHp. Si falta,tscda error al crear el objeto, y por esogeralt.showHp()se puede llamar sin revisar si existe. - Hay dos formas de escribirlo en una interfaz: como propiedad
(
showHp: () => void) o como método (showHp(): void). Parecen iguales pero no lo son: ver la pregunta 7.
this dentro del método. showHp() { ... } es la forma corta de un
método. Al llamarlo como geralt.showHp(), this es geralt, y TypeScript
sabe que this es un Character. Si se escribiera como flecha, this no
sería el objeto:
const bad: Character = {
name: 'Geralt',
hp: 70,
showHp: () => console.log(this.name) // error: Object is possibly 'undefined'.
}Una flecha usa el this de afuera; en el nivel superior de un módulo ese
this es undefined.
¿Por qué interface y no type? Para un objeto, los dos sirven. Poder
extenderse o implementarse no es exclusivo de las interfaces: una clase
puede hacer implements de un type que describa un objeto
(class Sq implements Shape funciona con type Shape = { area(): number }),
y una interfaz puede hacer extends de un type. Las diferencias reales son
dos: las interfaces se pueden fusionar (dos declaraciones con el mismo
nombre se combinan en una), y extends informa mejor los conflictos entre
propiedades que una intersección (&). Usar interface para objetos es una
convención común. La tabla completa está en interface o type del Módulo 2.
const MAX_HP = 100
const healCharacter = (character: Character, amount: number) => {
if (character.hp >= MAX_HP) { return }
character.hp = Math.min(character.hp + amount, MAX_HP)
}
healCharacter(geralt, 20) // 70 -> 90
healCharacter(geralt, 20) // 90 -> 100 (Math.min evita llegar a 110)
healCharacter(geralt, 20) // ya está en 100: early return, sigue en 100
geralt.showHp() // "Current HP of Geralt is: 100"-
El tipo atrapa errores de tipeo.
character.healtPoints += amountda error porquehealtPointsno existe enCharacter. En JavaScript esa línea crearía una propiedad nueva conNaNy no avisaría nada. -
Early return (o guard clause): salir de la función al principio cuando
no hay nada que hacer, en lugar de envolver todo en un
if. Hace el código más fácil de leer; el beneficio no es de rendimiento sino de claridad. -
La función recibe el mismo objeto, no una copia.
healCharacterno devuelve nada: modifica directamente elgeraltque le pasaron.
El error que tenía la primera versión. El ejemplo empezó con un tope que no funcionaba:
if (character.hp === 100) { return }
character.hp += amountLa condición sólo detenía la curación si el HP valía exactamente 100:
hp inicial 70
healCharacter(+20) 70 !== 100 -> suma -> 90
healCharacter(+20) 90 !== 100 -> suma -> 110 <- supera el máximo
La versión actual lo corrige así:
-
>=en lugar de===: también se detiene si el HP ya llegó por encima del máximo por otro camino. -
Math.min(hp + amount, MAX_HP): devuelve el menor de los dos valores, así que la suma nunca pasa del máximo. - Además,
MAX_HPreemplaza el100escrito a mano (un "número mágico"): si el máximo cambia, se cambia en un solo lugar.
TypeScript no detectó el error original: los tipos revisan qué datos usas, no si la lógica es correcta. Ambas versiones compilan igual. Para eso están las pruebas.
El archivo termina con export { } vacío. Esa línea convierte un archivo en
módulo; en este proyecto no hace falta, porque moduleDetection: "force" ya
trata cada archivo como módulo, pero no hace daño.
En Angular casi todo son métodos de clase y funciones flecha:
- Los métodos de un componente usan
thispara leer su estado. Si pasas un método como callback (setTimeout(this.save, 0)), pierde suthis; con una flecha (setTimeout(() => this.save(), 0)) lo conserva. - Los callbacks de RxJS y de signals (
map(x => x * 2),computed(() => ...)) son funciones flecha, y TypeScript infiere sus parámetros por contexto.
- Usar
||en lugar de??para valores por defecto:secondNumber || 1convierte un0legítimo en1, ymultiply(2, 0)daría4en lugar de0. - Pensar que un parámetro opcional ya tiene valor. Dentro de la función es
number | undefinedy hay que manejar elundefined. - Escribir una flecha con llaves y olvidar el
return:(a, b) => { a + b }devuelveundefined. - Definir un método de objeto como flecha (
showHp: () => console.log(this.name)):thisno es el objeto. - Comparar con
===contra un límite (hp === 100) cuando lo que se quiere es un tope: usa>=yMath.min.
Tipa siempre los parámetros; el retorno se puede inferir, pero anotarlo
documenta el contrato. Opcionales y por defecto van al final, y para valores
por defecto se usa ??. Los métodos de un objeto se escriben con la forma
corta (showHp() {}) para que this sea el objeto. Los tipos no revisan la
lógica: un tope mal escrito compila igual.
Respuesta:
-
this: la flecha toma elthisdel lugar donde se escribió (a eso se le llamathisléxico); la tradicional lo recibe según cómo se la llama. Por eso un método pasado como callback pierde suthisy una flecha no. -
arguments: la flecha no tiene su propio objetoarguments; se usan parámetros rest (...args). -
new: una flecha no se puede usar como constructor y no tieneprototype. -
Hoisting: una declaración
functionse puede llamar antes de la línea donde está escrita; una flecha asignada aconstno.
En una clase, un campo flecha (save = () => {...}) se crea por cada
instancia, mientras que un método vive una sola vez en el prototipo. Es un
costo menor, pero existe.
Respuesta:
Un tipo de función con retorno void significa "el que llama va a ignorar el
resultado", no "la función no puede devolver nada". Gracias a esa regla
funciona [1, 2].forEach(n => nums.push(n)): push devuelve un número, pero
forEach espera un callback void y simplemente lo descarta. La regla es
distinta cuando se declara una función con retorno void explícito
(function f(): void { return 42 }), que sí da error.
3. secondNumber?: number, secondNumber: number | undefined y secondNumber = 1: ¿en qué se diferencian?
Respuesta:
-
secondNumber?: number: el que llama puede omitirlo; dentro valenumber | undefined. -
secondNumber: number | undefined: el que llama debe pasarlo, aunque seaundefinedexplícito. -
secondNumber = 1: el que llama puede omitirlo y dentro siempre esnumber. Desde fuera la firma se ve igual que la opcional (secondNumber?: number).
En la mayoría de los casos el valor por defecto es lo más limpio, porque elimina
el undefined dentro de la función.
Respuesta:
La inferencia del retorno es confiable. Conviene anotarlo cuando:
- La función es parte de una API pública (exportada, un servicio): el tipo anotado es el contrato, y un cambio accidental en el cuerpo da error en la función y no en todos los lugares que la usan.
- La función es recursiva (se llama a sí misma): muchas veces TypeScript no
logra deducir el retorno y da error (
TS7023) hasta que lo anotas. - El proyecto usa
isolatedDeclarations, que exige retornos explícitos en lo exportado para generar archivos.d.tssin revisar todo el programa.
En funciones internas cortas y callbacks, la inferencia basta.
Respuesta:
Las sobrecargas declaran varias firmas para una sola implementación, para que el tipo de retorno dependa del argumento:
function parse(value: string): number
function parse(value: number): string
function parse(value: string | number): string | number {
return typeof value === 'string' ? Number(value) : String(value)
}Sirven cuando la relación entre entrada y salida importa al que llama. Si el
retorno es el mismo para todas las entradas, un parámetro de tipo unión
(value: string | number) es más simple. La implementación no es visible desde
fuera: sólo se pueden usar las firmas declaradas arriba. Por eso, si el
argumento es string | number, la llamada falla con
No overload matches this call, aunque la implementación lo acepte. Una alternativa
moderna son los genéricos con tipos condicionales, que se verán más adelante.
Respuesta:
En una función tradicional o un método, this depende de cómo se llama,
no de dónde se escribió:
-
geralt.showHp():thisesgeralt. -
const fn = geralt.showHp; fn():thisesundefined(en módulos y modo estricto), ythis.namefalla en runtime. -
fn.call(otro)ofn.bind(otro):thisesotro. - En una flecha,
thises el del lugar donde se escribió.
TypeScript ayuda de dos formas: con noImplicitThis (incluido en strict)
marca usos de this cuyo tipo no conoce, y permite declarar un parámetro
falso this para exigir cómo se llama una función:
function showHp(this: Character) { ... }. Ese parámetro desaparece al
compilar. Lo que TypeScript no detecta es extraer un método de una clase y
llamarlo suelto (const fn = obj.method); para eso hay reglas de lint como
unbound-method.
Respuesta:
Se usan igual, pero el compilador las revisa distinto. En simple: con la
sintaxis de propiedad, TypeScript no te deja guardar una función que acepta
menos cosas de las que promete la interfaz; con la sintaxis de método, sí.
El nombre técnico: con strictFunctionTypes (incluido en strict), los
parámetros de una propiedad función se revisan de forma estricta
(contravariante) y los de un método de forma más permisiva (bivariante):
interface WithProp { handle: (x: string | number) => void }
interface WithMethod { handle(x: string | number): void }
const onlyString = (x: string) => console.log(x)
const p: WithProp = { handle: onlyString } // error: number no es string
const m: WithMethod = { handle: onlyString } // compila, y puede fallar en runtimeonlyString no sabe manejar números, así que el error de p es correcto; la
sintaxis de método lo deja pasar por compatibilidad con código antiguo (por
ejemplo, los métodos de Array). Por eso muchas guías de estilo recomiendan la
sintaxis de propiedad, como la de este ejemplo.
Respuesta:
Modificar el objeto (mutación) es simple y no crea objetos nuevos, pero tiene
efectos a distancia: cualquier otra parte del código que tenga una referencia a
geralt ve el cambio sin haberlo pedido, y es más difícil de probar. Devolver
uno nuevo (return { ...character, hp: Math.min(character.hp + amount, MAX_HP) })
hace la función pura: misma entrada, misma salida, sin efectos.
En Angular esto importa en la práctica: con ChangeDetectionStrategy.OnPush y
con signals, la vista se actualiza cuando cambia la referencia. Si se muta
el objeto, la referencia es la misma y la pantalla puede no actualizarse. Por
eso en estado de UI se prefiere la inmutabilidad
(character.update(c => ({ ...c, hp: c.hp + 20 }))). Se puede reforzar con
readonly en la interfaz para que tsc impida la mutación.
Archivo: 01-typescript-intro/src/topics/04-homework-types.ts
Crear una interfaz
SuperHeroconname(string),age(number),address(un objeto constreet,countryycity) y un métodoshowAddressque devuelva un texto con el nombre y la dirección del héroe. Después crear un objeto que cumpla la interfaz y llamar ashowAddress.
Esta tarea junta lo visto en los módulos 2 y 3: interfaces, propiedades de tipo objeto y funciones dentro de una interfaz.
Los datos reales casi nunca son planos: un héroe tiene una dirección, un pedido tiene un cliente, un cliente tiene una dirección. Hace falta describir objetos dentro de objetos, y poder reutilizar la descripción de la parte interna (la dirección) en otros lugares.
-
Interfaz anidada: una propiedad cuyo tipo es otra interfaz
(
address: Address). - Composición: armar un tipo grande a partir de tipos pequeños, como piezas de Lego.
- Convención de nombres: reglas de escritura que no exige el compilador pero que todo el equipo sigue para que el código se lea igual.
interface Address {
street: string
country: string
city: string
}
interface SuperHero {
name: string
age: number
address: Address
showAddress: () => string
}
const superHeroe: SuperHero = {
name: "Spiderman",
age: 30,
address: {
street: "Main St",
country: "USA",
city: "NY",
},
showAddress() {
return this.name + ", " + this.address.city + ", " + this.address.country
},
}
console.log(superHeroe.showAddress()) // Spiderman, NY, USASuperHero
├── name: string
├── age: number
├── address: Address ──> Address
│ ├── street: string
│ ├── country: string
│ └── city: string
└── showAddress: () => string
Interfaz separada o tipo en línea. La dirección se podría escribir dentro
de SuperHero:
interface SuperHero {
address: { street: string; country: string; city: string } // tipo en línea
}Funciona igual, pero separarla en Address tiene ventajas: se reutiliza (un
Villain o un Company también tienen dirección), tiene nombre propio en los
mensajes de error y se puede usar como tipo de un parámetro
(function formatAddress(a: Address)).
Las revisiones llegan a los objetos internos. TypeScript revisa cada
nivel, también dentro de address:
address: { street: 's', country: 'c' }
// error: Property 'city' is missing ... but required in type 'Address'.
address: { street: 's', country: 'c', city: 'x', zip: '1' }
// error: Object literal may only specify known properties, and 'zip' does not exist in type 'Address'.El tipo de retorno del método también se revisa. showAddress: () => string
obliga a devolver un texto. Si el método no tiene return, tsc da
Type '() => void' is not assignable to type '() => string'.
this dentro de showAddress. Igual que en el Módulo 3: con la forma
corta showAddress() { ... }, this es el objeto y TypeScript sabe que es un
SuperHero. Con una flecha, this no sería el héroe.
En TypeScript, todos los tipos se escriben en UpperCamelCase (también llamado PascalCase), no sólo las interfaces:
| Qué se nombra | Convención | Ejemplo |
|---|---|---|
Interfaces, alias type, clases, enum
|
PascalCase |
SuperHero, LifeStatus
|
| Variables, funciones, métodos, propiedades | camelCase |
showAddress, hpPoints
|
| Constantes fijas de configuración | UPPER_SNAKE_CASE | MAX_HP |
Miembros de un enum
|
PascalCase o UPPER_SNAKE_CASE |
Status.Full, FULL
|
Así, con sólo leer SuperHero sabes que es un tipo, y con superHero que es
un valor. No se usa el prefijo I (ISuperHero): la guía de estilo de
Angular y la de Google para TypeScript lo desaconsejan.
El código compila y funciona, pero tiene tres diferencias con las convenciones del resto del repositorio:
-
Comillas dobles (
"Spiderman"). El repositorio usa comillas simples. -
superHeroemezcla inglés y español. Siguiendo la regla de código en inglés, seríasuperHero. -
Concatenación con
+. Un template literal (Módulo 3) se lee mejor:
showAddress() {
return `${this.name}, ${this.address.city}, ${this.address.country}`
}Así se modelan los datos de una API en Angular: una interfaz por entidad y
otras para sus partes (Address, Order, OrderItem). Pero hay una trampa:
los datos que llegan de una API son JSON, y JSON no tiene funciones. Si
una interfaz de datos incluye un método, como showAddress, el objeto que
devuelve http.get<SuperHero>() no lo tendrá. Ver la pregunta 4.
- Repetir la misma forma en línea en varias interfaces en lugar de extraerla a un tipo con nombre.
- Creer que
Readonly<SuperHero>oPartial<SuperHero>llegan a los objetos internos. Sólo actúan sobre el primer nivel (preguntas 2 y 3). - Poner métodos en interfaces que describen datos de una API.
Las interfaces se componen: una propiedad puede tener como tipo otra interfaz, y TypeScript revisa todos los niveles. Todos los tipos van en PascalCase y todos los valores en camelCase.
Respuesta:
Se extrae cuando se reutiliza en más de un lugar, cuando tiene significado
propio en el dominio (una Address existe aunque no haya héroe), cuando se
usa como parámetro o retorno de una función, o cuando los mensajes de error
con el tipo en línea se vuelven ilegibles. Se deja en línea cuando es pequeño,
se usa en un solo lugar y no tiene sentido fuera de su contenedor (por
ejemplo, las opciones de una única función). Si después hace falta el tipo
interno, se puede obtener sin duplicarlo: type Address = SuperHero['address'].
Respuesta:
No. Readonly<T> es superficial: marca como readonly sólo las
propiedades del primer nivel.
const hero: Readonly<SuperHero> = superHeroe
hero.name = 'Batman' // error: Cannot assign to 'name' because it is a read-only property.
hero.address.city = 'LA' // compila: address es readonly, pero su contenido noPara proteger todos los niveles hay que marcar readonly también en
Address, o escribir un tipo recursivo (DeepReadonly<T>) con tipos
mapeados y condicionales. Object.freeze tiene la misma limitación en
runtime. Para estado de UI en Angular, la práctica habitual es no mutar y
crear objetos nuevos con spread en cada nivel que cambia.
Respuesta:
No del todo, porque Partial<T> también es superficial: hace opcional
address, pero si la pasas, tiene que ser una Address completa.
const patch: Partial<SuperHero> = { address: { city: 'LA' } }
// error: Type '{ city: string; }' is missing the following properties from type 'Address': street, countryOpciones: un tipo específico para la operación
({ address?: Partial<Address> }), un DeepPartial<T> recursivo, o derivar
tipos con Pick y Omit
(type HeroSummary = Pick<SuperHero, 'name' | 'age'>). En APIs suele ser más
claro un tipo por operación (UpdateHeroAddress) que un DeepPartial
genérico, porque documenta exactamente qué se puede cambiar.
Respuesta:
JSON sólo transporta datos: textos, números, booleanos, arrays y objetos. No tiene funciones. El código compila, pero falla al ejecutarse:
const hero = JSON.parse(body) as SuperHero // compila: as no valida nada
hero.showAddress() // TypeError: hero.showAddress is not a functionLo mismo pasa con http.get<SuperHero>() en Angular: el genérico es sólo una
promesa al compilador. La solución es separar datos de comportamiento:
una interfaz sólo con datos para lo que viene de la API (SuperHeroDto) y
funciones aparte que trabajan sobre ella
(formatAddress(hero: SuperHeroDto): string), o una clase que se construye a
partir del DTO después de validarlo. Para validar en runtime, ver la Introducción.
Respuesta:
TypeScript separa dos mundos: el de los tipos (se borran al compilar) y el
de los valores (existen al ejecutar). Un mismo nombre puede existir en los dos (por ejemplo, una clase
es a la vez un tipo y un valor). La convención de mayúsculas hace visible en
cuál estás: SuperHero es un tipo, superHero es un valor. El prefijo I
(ISuperHero) viene de C#; la guía de estilo de Angular y la de Google para
TypeScript lo desaconsejan porque al que usa el tipo no le importa si es una interface o
un type, y si cambias uno por el otro tendrías que renombrarlo en todo el
código. Lo importante, en cualquier caso, es que el equipo sea consistente; un
linter (@typescript-eslint/naming-convention) puede exigirlo.
Archivo: 01-typescript-intro/src/topics/05-basic-destructuring.ts
Para usar varias propiedades de un objeto hay que repetir su nombre una y otra vez:
console.log(audioPlayer.audioVolume, audioPlayer.songDuration, audioPlayer.details.author)La desestructuración saca esas propiedades a variables con una sola línea, y el código que sigue queda más corto y más fácil de leer.
Desestructurar es extraer valores de un objeto (o de un array) y
guardarlos en variables, escribiendo a la izquierda del = la forma de lo que
quieres sacar. Es JavaScript (ES2015), no algo propio de TypeScript;
TypeScript sólo agrega los tipos.
interface Details {
author: string
year: number
}
interface AudioPlayer {
audioVolume: number
songDuration: number
songTitle: string
details: Details
}
const audioPlayer: AudioPlayer = {
audioVolume: 90,
songDuration: 36,
songTitle: 'Falling away from me',
details: { author: 'Korn', year: 1999 }
}
const songTitle = 'Freak on a Leash'
const { audioVolume, songDuration, songTitle: anotherTitle, details: { author, year } } = audioPlayerEsa última línea crea cinco variables:
patrón variable creada valor
audioVolume audioVolume 90
songDuration songDuration 36
songTitle: anotherTitle anotherTitle 'Falling away from me'
details: { author, year } author 'Korn'
year 1999
(details NO se crea)
1. Extraer con el mismo nombre. { audioVolume } busca la propiedad
audioVolume y crea una variable con ese nombre. TypeScript infiere su tipo
(number) desde AudioPlayer; no hace falta anotar nada.
2. Renombrar. songTitle: anotherTitle se lee "toma la propiedad
songTitle y guárdala en una variable llamada anotherTitle". Aquí hace
falta porque ya existe una variable songTitle; sin renombrar, se declararía
dos veces:
const songTitle = 'Freak on a Leash'
const { songTitle } = audioPlayer
// error TS2451: Cannot redeclare block-scoped variable 'songTitle'.Cuidado: dentro de un patrón, los dos puntos renombran; no anotan el tipo.
const { songTitle: string } = audioPlayer
// NO declara un string: crea una variable llamada `string`
const { songTitle }: AudioPlayer = audioPlayer // así se anota (casi nunca hace falta)3. Desestructuración anidada. details: { author, year } se lee "entra en
details y saca author y year". Los dos puntos aquí tampoco son un tipo:
indican en qué propiedad entrar. El patrón crea author y year, pero no
details. Por eso el archivo puede declarar details después sin conflicto.
4. La alternativa en dos pasos. El archivo muestra la misma extracción sin anidar:
const { details } = audioPlayer
const { author: songAuthor } = detailsCon objetos profundos, dos líneas simples se leen mejor que un patrón anidado de tres niveles.
Valores por defecto. Se usan cuando la propiedad vale undefined:
const { audioVolume = 50 } = settingsOjo: el valor por defecto sólo se aplica con undefined. Si la propiedad vale
null o 0, se queda con ese valor (ver la pregunta 2).
Resto (...). Junta en un objeto nuevo las propiedades que no extrajiste:
const { details, ...rest } = audioPlayer
// rest: { audioVolume: number; songDuration: number; songTitle: string }TypeScript calcula el tipo de rest sin details.
En parámetros de funciones. Es el uso más común en código real:
function play({ songTitle, audioVolume = 50 }: AudioPlayer) {
console.log(`Playing ${songTitle} at ${audioVolume}%`)
}El tipo va después del patrón completo (}: AudioPlayer), no dentro.
Arrays. También se desestructuran, por posición en lugar de por nombre:
const [first, , third = 'z'] = ['a', 'b'] // first = 'a', se salta 'b', third = 'z'
;[x, y] = [y, x] // intercambia dos variablesEn Angular aparece todo el tiempo: en parámetros
(map(({ id, name }) => ...) en RxJS), al leer respuestas de API y al leer
signals (const { name } = this.user()). Con signals hay un detalle
importante: desestructurar toma una foto del valor en ese momento. Si el
signal cambia después, name no se entera. Para que se actualice, la lectura
tiene que estar dentro de un computed() o en el template.
- Leer
{ songTitle: string }como una anotación de tipo. Es un renombre. - Creer que el patrón anidado crea también la variable del objeto padre
(
details). No la crea. - Desestructurar una propiedad anidada que puede no existir: si
detailsfuera opcional,const { details: { author } } = objfalla.tsclo avisa (Property 'author' does not exist on type 'Details | undefined') y, si se ignora, en runtime daTypeError: Cannot read properties of undefined. - Creer que desestructurar copia los objetos.
const { details } = audioPlayercopia la referencia: si modificasdetails.author, cambia tambiénaudioPlayer.details.author.
{ a } extrae, { a: b } renombra, { a: { b } } entra en a y extrae b.
Los dos puntos dentro del patrón nunca son un tipo: el tipo va después del
patrón completo, y casi siempre se infiere solo.
Respuesta:
Porque dentro de un patrón de desestructuración, : significa "renombrar":
function f({ title: string }) crea una variable llamada string y deja el
parámetro sin tipo (con strict, eso da error de any implícito). El tipo va
después del patrón completo:
function play({ songTitle, audioVolume = 50 }: AudioPlayer) { ... }
// o con un tipo en línea:
function play({ songTitle }: { songTitle: string }) { ... }En funciones con muchas opciones es común definir una interfaz para el
parámetro (PlayOptions), porque el patrón y el tipo en línea juntos se
vuelven difíciles de leer.
Respuesta:
Sólo cuando el valor es undefined (o la propiedad no existe):
const { a = 1 } = { a: undefined } // a = 1
const { b = 1 } = { b: null } // b = null
const { c = 1 } = { c: 0 } // c = 0Es la misma regla que en los parámetros por defecto de una función. Es
distinto de ??, que también reemplaza null. Las APIs suelen devolver
null para "sin valor", así que un default en la desestructuración no lo
cubre: hace falta const b = data.b ?? 1. TypeScript lo refleja en el tipo:
si b era number | null, sigue siendo number | null.
Respuesta:
Copia los valores de primer nivel. Para primitivos (number, string) es
una copia real; para objetos se copia la referencia:
const { details } = audioPlayer
details.author = 'X'
console.log(audioPlayer.details.author) // 'X': es el mismo objetoLo mismo pasa con el resto (...rest) y con el spread ({ ...obj }): son
copias superficiales. Para actualizar estado de forma inmutable (por
ejemplo, en un signal de Angular) hay que hacer spread en cada nivel que
cambia: { ...player, details: { ...player.details, author: 'X' } }. Para
copias profundas existe structuredClone().
Respuesta:
Con desestructuración y resto:
const { details, ...withoutDetails } = audioPlayerwithoutDetails es un objeto nuevo sin details, y TypeScript le asigna un
tipo equivalente a Omit<AudioPlayer, 'details'>. Es la forma habitual de
quitar datos sensibles antes de enviar o registrar un objeto
(const { password, ...safeUser } = user). La variable extraída (details,
password) queda sin usar, pero noUnusedLocals de TypeScript no la
reporta: entiende que está ahí para excluirla del resto. Quien sí suele
avisar es ESLint (no-unused-vars), salvo que se active su opción
ignoreRestSiblings.
Respuesta:
Desestructurar lee el valor una vez. const { name } = this.user() guarda
el nombre de ese momento; si el signal user cambia, name queda
desactualizado y la vista no reacciona. La lectura tiene que ocurrir donde
Angular pueda seguirla: dentro de un computed(() => this.user().name), de
un effect() o en el template. Con RxJS pasa lo mismo: se desestructura
dentro del map(({ name }) => ...), para que se ejecute con cada valor nuevo,
no una vez fuera del flujo.
Referencia rápida para consultar mientras escribes código. Cubre los mismos temas que la hoja de atajos de Angular de Fernando Herrera, reescritos y puestos al día: primero va la forma actual y después, cuando existe, la forma antigua que todavía vas a ver en proyectos existentes.
npm install -g @angular/cli # instala el CLI
ng new my-app # crea un proyecto
ng serve # servidor de desarrollo
ng build # compila para producción
ng generate --help # ayuda de cualquier comandoGenerar piezas (g es la forma corta de generate):
| Comando | Corto | Qué crea |
|---|---|---|
ng g component <nombre> |
ng g c |
Un componente. El nombre puede ser una ruta (heroes/hero-card) |
ng g directive <nombre> |
ng g d |
Una directiva |
ng g pipe <nombre> |
ng g p |
Un pipe |
ng g service <nombre> |
ng g s |
Un servicio |
ng g guard <nombre> |
ng g g |
Un guard para proteger rutas |
ng g interceptor <nombre> |
Un interceptor de peticiones HTTP |
Banderas útiles:
| Bandera | Qué hace |
|---|---|
--dry-run |
Muestra qué archivos crearía, sin crear nada. Úsala si dudas |
--flat |
Crea los archivos en la carpeta actual, sin subcarpeta nueva |
--inline-style (-s) |
Pone los estilos dentro del archivo .ts
|
--inline-template (-t) |
Pone el HTML dentro del archivo .ts
|
--skip-tests |
No crea el archivo de pruebas |
| Sintaxis | Qué hace |
|---|---|
{{ heroName }} |
Muestra el valor en pantalla (interpolación) |
<input [value]="firstName"> |
Enlaza una propiedad del elemento con un valor de la clase |
<div [attr.role]="ariaRole"> |
Enlaza un atributo HTML |
<div [class.active]="isActive"> |
Pone la clase active sólo si la expresión es verdadera |
<div [style.width.px]="size"> |
Enlaza un estilo, con su unidad |
<button (click)="save($event)"> |
Escucha un evento y llama a un método |
<app-title [(title)]="name" /> |
Enlace en los dos sentidos: lee y escribe |
{{ heroName | uppercase }} |
Pasa el valor por un pipe antes de mostrarlo |
{{ hero?.name }} |
Navegación segura: no falla si hero es null
|
<input #search> |
Referencia local: search nombra ese elemento en el template |
El enlace en los dos sentidos es una forma corta de escribir esto:
<app-title [title]="name" (titleChange)="name = $event" />Regla para recordar: corchetes [] = el dato entra al elemento,
paréntesis () = el evento sale, los dos juntos [()] = ida y vuelta.
@if (isAdmin) {
<button>Borrar</button>
} @else {
<p>No tienes permiso.</p>
}
<ul>
@for (hero of heroes; track hero.id) {
<li>{{ $index + 1 }}. {{ hero.name }}</li>
} @empty {
<li>No hay héroes.</li>
}
</ul>
@switch (status) {
@case ('FULL') { <span>Sano</span> }
@case ('INJURED') { <span>Herido</span> }
@default { <span>Desconocido</span> }
}-
trackes obligatorio en@for. Le dice a Angular cómo reconocer cada elemento para reutilizarlo cuando la lista cambia. Usa algo único, como elid. -
@emptyse muestra cuando la lista no tiene elementos.
Variables disponibles dentro de @for:
| Variable | Valor |
|---|---|
$index |
Posición del elemento (empieza en 0) |
$count |
Total de elementos |
$first / $last
|
true si es el primero / el último |
$even / $odd
|
true si la posición es par / impar |
Forma antigua (directivas de CommonModule). Hacen lo mismo y siguen
apareciendo en proyectos existentes:
<section *ngIf="showSection">...</section>
<li *ngFor="let hero of heroes">{{ hero.name }}</li>
<div [ngClass]="{ active: isActive, disabled: isDisabled }"></div>
<div [ngStyle]="{ color: textColor }"></div>
<input [(ngModel)]="userName"> <!-- necesita FormsModule -->export class HeroCard {
hero = input.required<Hero>() // dato que entra desde el padre
selected = output<Hero>() // evento que sale hacia el padre
select() {
this.selected.emit(this.hero())
}
}<app-hero-card [hero]="geralt" (selected)="onSelect($event)" />| Actual (funciones) | Antigua (decoradores) | Para qué sirve |
|---|---|---|
input() |
@Input() |
Recibir un dato del componente padre |
output() |
@Output() x = new EventEmitter() |
Avisar al padre de que pasó algo |
model() |
@Input() + @Output() con sufijo Change
|
Enlace en los dos sentidos ([(x)]) |
viewChild() |
@ViewChild() |
Obtener un elemento o componente del propio template |
host: { ... } en @Component
|
@HostBinding() / @HostListener()
|
Poner clases o escuchar eventos en la etiqueta del propio componente |
Ojo: un input() es un signal, así que se lee con paréntesis (this.hero()).
Métodos que Angular llama solo, en este orden, mientras un componente vive:
| Método | Cuándo se llama |
|---|---|
constructor |
Al crear la clase. Los datos del padre todavía no llegaron |
ngOnChanges |
Cada vez que cambia un dato de entrada. La primera vez, antes de ngOnInit
|
ngOnInit |
Una sola vez, cuando ya llegaron los primeros datos de entrada |
ngDoCheck |
En cada revisión de cambios. Corre muy seguido: evita trabajo pesado |
ngAfterContentInit |
Una vez, cuando está listo el contenido que el padre proyectó (<ng-content>) |
ngAfterContentChecked |
Después de cada revisión de ese contenido |
ngAfterViewInit |
Una vez, cuando el template propio y los hijos ya están en pantalla |
ngAfterViewChecked |
Después de cada revisión del template propio |
ngOnDestroy |
Justo antes de destruir el componente. Aquí se limpia lo que se abrió |
Los que más se usan: ngOnInit para cargar datos y ngOnDestroy para
limpiar.
export const routes: Routes = [
{ path: '', component: Home },
{ path: 'heroes/:id', component: HeroDetail }, // :id es un parámetro
{ path: 'old-heroes', redirectTo: 'heroes' },
{
path: 'admin',
loadComponent: () => import('./admin/admin').then(m => m.Admin), // lazy loading
canActivate: [authGuard]
},
{ path: '**', component: NotFound } // cualquier otra ruta
]Las rutas se registran con provideRouter(routes) en la configuración de la
aplicación, y <router-outlet /> marca el lugar del template donde se dibuja
la ruta activa.
Enlaces:
<a routerLink="/heroes">Héroes</a>
<a [routerLink]="['/heroes', hero.id]">Detalle</a>
<a [routerLink]="['/heroes']" [queryParams]="{ page: 1 }">Página 1</a>
<a [routerLink]="['/heroes']" fragment="top">Arriba</a>
<a routerLink="/heroes" routerLinkActive="active">Héroes</a>routerLinkActive agrega la clase indicada cuando esa ruta es la actual; se
usa para resaltar el menú.
Forma antigua: RouterModule.forRoot(routes) dentro de un NgModule, y
loadChildren apuntando a un módulo para el lazy loading.
Un guard es una función que responde «¿se puede pasar?» antes de entrar o salir de una ruta.
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService)
const router = inject(Router)
return auth.isLoggedIn() || router.createUrlTree(['/login'])
}Lo que devuelve decide qué pasa: true deja pasar, false bloquea, y un
UrlTree redirige a otra ruta. También puede devolver una promesa o un
observable con cualquiera de esos valores.
| Tipo | Qué decide |
|---|---|
CanActivateFn |
Si se puede entrar a la ruta |
CanActivateChildFn |
Si se puede entrar a las rutas hijas |
CanDeactivateFn |
Si se puede salir. Útil cuando hay cambios sin guardar |
CanMatchFn |
Si la ruta cuenta como coincidencia. Evita incluso descargar su código |
ResolveFn |
No bloquea: trae datos antes de mostrar la ruta |
Forma antigua: clases con @Injectable() que implementan CanActivate,
CanDeactivate o CanLoad. Están en desuso; CanMatchFn reemplaza a
CanLoad.
Un pipe cambia cómo se muestra un dato, sin tocar el dato original.
| Pipe | Qué hace |
|---|---|
date |
Da formato a una fecha |
uppercase / lowercase
|
Pasa todo el texto a mayúsculas / minúsculas |
titlecase |
Pone en mayúscula la primera letra de cada palabra |
currency |
Muestra un número como dinero |
number |
Da formato a un número (decimales, separadores) |
percent |
Muestra un número como porcentaje |
async |
Espera una promesa o un observable y muestra lo que llega. Cierra la suscripción solo |
json |
Muestra un objeto como JSON. Útil para depurar |
keyvalue |
Convierte un objeto en una lista de pares clave y valor para recorrerla |
slice |
Toma una parte de una lista o de un texto |
i18nPlural |
Elige un texto según una cantidad («1 mensaje», «3 mensajes») |
i18nSelect |
Elige un texto según un valor (por ejemplo, el idioma o el género) |
Un signal es una caja que guarda un valor y avisa a quien lo usa cuando ese valor cambia. Así Angular sabe exactamente qué parte de la pantalla tiene que actualizar.
export class Counter {
count = signal(0) // signal que se puede escribir
doubleCount = computed(() => this.count() * 2) // se recalcula solo
increment() {
this.count.update(value => value + 1) // a partir del valor anterior
}
reset() {
this.count.set(0) // valor nuevo, sin mirar el anterior
}
}| Qué | Para qué |
|---|---|
signal(0) |
Crea un signal con un valor inicial |
count() |
Lee el valor. Siempre con paréntesis |
count.set(10) |
Reemplaza el valor |
count.update(v => v + 1) |
Calcula el valor nuevo a partir del anterior |
computed(() => ...) |
Signal de sólo lectura que depende de otros y se recalcula solo |
effect(() => ...) |
Código que se vuelve a ejecutar cuando cambia un signal que lee |
Ojo con los objetos y arrays: no los modifiques por dentro. Si la referencia es la misma, el signal no avisa. Crea uno nuevo:
todos.update(list => list.map(todo => ({ ...todo, done: true })))Effects. Corren al menos una vez y se repiten cuando cambia alguno de los signals que leen. Se crean en el constructor o como propiedad de la clase, y se destruyen solos junto con el componente.
constructor() {
effect((onCleanup) => {
const timer = setTimeout(() => console.log(this.count()), 1000)
onCleanup(() => clearTimeout(timer)) // limpia antes de volver a correr
})
}- Úsalos para: registrar en consola o en analíticas, guardar en
localStorage, dibujar en un canvas, conectar librerías externas. - No los uses para copiar un signal en otro. Para eso está
computed; hacerlo con un effect produce ciclos y errores de detección de cambios.
@defer deja para más tarde la descarga de un pedazo de pantalla, y te deja
elegir el momento.
@defer (on viewport; prefetch on idle) {
<app-calendar />
} @placeholder (minimum 500ms) {
<p>Aquí va el calendario</p>
} @loading (after 100ms; minimum 1s) {
<p>Cargando...</p>
} @error {
<p>No se pudo cargar el calendario.</p>
}| Bloque | Qué muestra |
|---|---|
@defer |
El contenido pesado, cuando se cumple la condición |
@placeholder |
Lo que se ve antes de empezar a cargar |
@loading |
Lo que se ve mientras se descarga |
@error |
Lo que se ve si la descarga falla |
Cuándo se dispara la carga:
| Condición | Se dispara cuando... |
|---|---|
on idle |
El navegador queda libre. Es la opción por defecto |
on viewport |
El bloque entra en el área visible |
on interaction |
Alguien hace clic o escribe sobre el elemento |
on hover |
El mouse pasa por encima |
on immediate |
Apenas termina de dibujarse el resto de la página |
on timer(5s) |
Pasa el tiempo indicado |
when <condición> |
La expresión se vuelve verdadera |
- Se pueden combinar con
;. Alcanza con que se cumpla una. -
on interaction(greeting)yon hover(greeting)miran otro elemento, marcado con#greeting, en lugar del placeholder. -
prefetch on idledescarga el código por adelantado, sin dibujarlo, para que aparezca al instante cuando llegue el momento.
| Término | En simple |
|---|---|
| Angular / AngularJS | Son dos cosas distintas. AngularJS es el antecesor y ya no tiene soporte |
| Componente | Una pieza de pantalla: una clase TypeScript, un template HTML y estilos opcionales |
| Directiva | Cambia el comportamiento de un elemento HTML o de un componente |
| Pipe | Cambia cómo se muestra un dato, sin modificar el dato |
| Servicio | Clase con lógica o datos que se comparten entre varios componentes |
| Inyección de dependencias (DI) | La clase no crea lo que necesita: lo pide, y Angular se lo entrega ya creado |
| Guard | Portero de una ruta: decide si se puede entrar, cargar o salir |
| Interceptor | Revisa o modifica las peticiones HTTP y sus respuestas al pasar |
| Observable | Objeto que va entregando valores a lo largo del tiempo. Suscribirse es quedarse escuchándolos |
| RxJS | La librería de observables que usa Angular |
Módulo (NgModule) |
Agrupador de componentes relacionados. Opcional desde los componentes standalone |
| Detección de cambios | El mecanismo que mantiene la pantalla igual al estado de la aplicación |
| Decorador | Etiqueta con @ que le dice a Angular qué es una clase (ver «Qué es Angular») |
| Polyfill | Código que agrega a un navegador una función que no trae |
Convenciones de nombres: las de TypeScript están en el Módulo 4. Angular suma
una: kebab-case (hero-card) para nombres de archivo y selectores de
componentes.
-
01-typescript-intro/README.md: cómo ejecutar el proyecto y agregar lecciones. -
01-typescript-intro/tsconfig.json: reglas del compilador. -
01-typescript-intro/src/topics/: un archivo por módulo de esta guía.