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
| 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 |
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.
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? El comentario del archivo da dos motivos.
Uno es correcto: las interfaces se pueden fusionar (dos declaraciones con
el mismo nombre se combinan en una). El otro hay que matizarlo: una clase
puede hacer implements de una interfaz, pero también de un type que
describa un objeto (class Sq implements Shape funciona con
type Shape = { area(): number }). La diferencia real está en la tabla de
interface o type del Módulo 2.
const healCharacter = (character: Character, amount: number) => {
if (character.hp === 100) { return }
character.hp += amount
}-
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. -
Los objetos se pasan por referencia.
healCharacterno devuelve nada: modifica el mismo objetogeraltque recibió.
Cuidado: este ejemplo no limita la vida a 100. La condición sólo detiene la curación si el HP vale exactamente 100:
hp inicial 70
healCharacter(+20) 70 !== 100 -> suma -> 90
healCharacter(+20) 90 !== 100 -> suma -> 110 <- supera el máximo
geralt.showHp() "Current HP of Geralt is: 110"
El comentario del archivo dice que la segunda llamada deja el HP en 90, pero al ejecutarlo da 110. Para poner un tope real:
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)
}
// 70 -> 90 -> 100>= cubre también un HP que ya llegó por encima del máximo por otro camino, y
Math.min asegura que la suma nunca lo pase. TypeScript no detecta este tipo
de error: los tipos revisan qué datos usas, no si la lógica es
correcta. 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ó (lé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, porque TypeScript no puede inferirla.
- 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. Con
strictFunctionTypes (incluido en strict), los parámetros de una propiedad
función se revisan de forma estricta (contravariante), mientras que los de un
método se revisan 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.
-
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.