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
| 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 |
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.
TypeScript es JavaScript con anotaciones de tipo. Todo JavaScript válido es casi siempre TypeScript válido; TypeScript sólo agrega información para que el compilador pueda revisar el código.
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 (type erasure). En el navegador no
queda ningún string, interface ni type. Por eso TypeScript no puede
validar datos que llegan en runtime (una API, un formulario): eso requiere
código real (type guards o librerías de validación).
-
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 usa el mismo compilador de TypeScript, pero envuelto en su propio
compilador (ngc), que además revisa los templates HTML. Lo que aprendas aquí
aplica directamente a 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 validar en runtime en el borde del
sistema: 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 no se desincronizan. Dentro del sistema,
una vez validado, el tipo ya es confiable.
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 herramientas que transpilan archivo por archivo no pueden saber si un
import es un tipo o un valor que vive en otro archivo. verbatimModuleSyntax
obliga a escribir import type { X } para los tipos, así el transpilador
puede borrar ese import sin mirar otros archivos. Este repositorio lo tiene
activo.
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 (
'FULL'). -
Unión (
|): un valor puede ser de uno de varios tipos. -
Alias de tipo (
type): un nombre para un tipo.
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.
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. En el archivo, const name: string pierde el
tipo literal porque la anotación lo ensancha a string a propósito.
Narrowing (estrechamiento). 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" (unión vacía)
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.
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 sin
valores: el retorno de una función que siempre lanza, o la rama imposible en
un chequeo exhaustivo. 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. Losenumnuméricos además aceptan cualquier número. -
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.
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. Cualquier objeto
con las propiedades de Character es 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 literales recién escritos, porque ahí casi siempre indica 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 sección 0).
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 cachea mejor las relaciones entre interfaces. 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 heurística para
detectar errores de tipeo, no una garantía de que el objeto no tenga
propiedades extra; en runtime 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:
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.
-
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.