Skip to content

study guide

Janio Isacura edited this page Oct 4, 2026 · 50 revisions

Guía de estudio: Angular UFH

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.

Cómo usar esta guía

  1. Cada proyecto del repositorio (01-typescript-intro/, …) es una parte de esta guía.
  2. Cada archivo de src/topics/ es un módulo. El número del módulo coincide con el número del archivo.
  3. Cada módulo termina con preguntas de entrevista de nivel senior, cada una con su respuesta debajo. Intenta responder antes de leerla.
  4. Si algo no te queda claro, cámbialo en el archivo del tópico, ejecuta npx tsc y mira qué error aparece. El compilador es el mejor profesor.

Mapa general

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

Ruta recomendada

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

Parte 1 · 0. Qué es TypeScript

El problema que resuelve

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 runtime

TypeScript agrega un sistema de tipos que se revisa antes de ejecutar. El mismo error aparece en el editor, mientras escribes.

Definición simple

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.

Cómo funciona por dentro

  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).

Cómo se aplica en este repositorio

  • npm run dev levanta Vite. Vite no revisa tipos: si hay un error de tipos, la página carga igual.
  • npx tsc es quien revisa tipos. Ejecútalo seguido.
  • TypeScript 6.0 activa el modo strict por defecto, aunque tsconfig.json no lo diga. Eso incluye noImplicitAny y strictNullChecks.
  • noUnusedLocals está activo: una variable de ejemplo que no se usa rompe el build. Por eso cada tópico termina con export { ... }.

Comparación con Angular

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.

Errores comunes

  • 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.

Resumen

TypeScript revisa antes de ejecutar y desaparece al ejecutar. tsc revisa, Vite compila.

Preguntas de entrevista (senior)

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.

2. ¿Qué diferencia hay entre revisar tipos y transpilar? ¿Por qué Vite no revisa tipos?

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).

3. ¿Qué activa strict y por qué conviene en un proyecto nuevo?

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.

4. ¿Qué es isolatedModules / verbatimModuleSyntax y por qué existen?

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.


Parte 1 · Módulo 1. Tipos básicos, literales y uniones

Archivo: 01-typescript-intro/src/topics/01-basic-types.ts

El problema que resuelve

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.

Definición simple

  • 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.

El código del repositorio

const name: string = 'Titus'
const isAlive: boolean = true
type LifeStatus = 'FULL' | 'DEAD' | 'INJURED'

let hpPoints: number | LifeStatus = 39

hpPoints = 'FULL'
  • LifeStatus es una unión de tres tipos literales. Sólo esos tres textos son válidos.
  • hpPoints acepta cualquier number o uno de esos tres textos.
  • hpPoints = 'FUL' daría error en tsc.

Cómo funciona por dentro

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)

Comparación con JavaScript

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.

Detalle de este proyecto: name

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.

Errores comunes

  • Usar any para "salir del paso". Usa unknown y 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 number distingue enteros de decimales. Es un único tipo de punto flotante (0.1 + 0.2 !== 0.3). Para enteros grandes existe bigint.

Resumen

Anota lo necesario, deja que la inferencia haga el resto, y usa uniones de literales para limitar los valores posibles.

Preguntas de entrevista (senior)

1. Diferencia entre any, unknown y never. ¿Cuándo usarías cada uno?

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.

2. ¿Qué tipo infiere TypeScript para const x = 'a' y para let x = 'a'? ¿Por qué?

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.

3. ¿Cómo garantizas que un switch sobre una unión cubre todos los casos?

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.

4. Unión de literales vs enum vs objeto as const: ¿qué eliges y por qué?

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 como erasableSyntaxOnly o el modo de "sólo quitar tipos" de Node la rechazan. Los enum numé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.

5. ¿Qué cambia strictNullChecks?

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.


Parte 1 · Módulo 2. Arrays tipados e interfaces

Archivo: 01-typescript-intro/src/topics/02-object-interface.ts

El problema que resuelve

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.

Definición simple

  • Array tipado: string[] es una lista donde todos los elementos son string.
  • Interfaz (interface): describe la forma de un objeto, es decir, qué propiedades tiene y de qué tipo.
  • Propiedad opcional (?): puede estar o no estar.

El código del repositorio

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ón string[] obliga a que todos sean texto.
  • geralt no tiene age al crearse, y es válido porque age es opcional.
  • geralt.age = 100 funciona aunque geralt sea const: const impide reasignar la variable, no modificar el objeto.

Cómo funciona por dentro

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 undefined

Comparación con Angular

En 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 o type

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.

Errores comunes

  • Pensar que const hace el objeto inmutable. Para eso existe readonly (readonly name: string, readonly string[]) o Object.freeze en 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 revisar undefined en todos los lugares donde se usa.

Resumen

Las interfaces describen formas; TypeScript compara formas, no nombres. Los literales escritos directamente reciben una revisión extra de propiedades.

Preguntas de entrevista (senior)

1. interface vs type: ¿cuándo usas cada uno?

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.

3. ¿Qué es el tipado estructural y cómo simularías tipado nominal?

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 UserId

Así un OrderId no se puede pasar donde se espera un UserId.

4. ¿Diferencia entre age?: number y age: number | undefined?

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.

5. const, readonly y Object.freeze: ¿qué protege cada uno?

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.


Parte 1 · Módulo 3. Funciones y parámetros

Archivo: 01-typescript-intro/src/topics/03-functions.ts

El problema que resuelve

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.

Definición simple

  • 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 vale undefined.
  • Parámetro por defecto (base: number = 2): si se omite, toma ese valor.
  • void: la función no devuelve nada útil.

Funciones tradicionales

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.

Funciones flecha

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 return es implícito (=> a + b). Con llaves hay que escribir return.
  • Las comillas invertidas son un template literal: ${a + b} inserta el resultado dentro del texto.
  • La diferencia importante con function es this: una función flecha no tiene su propio this, usa el del lugar donde se escribió. Ver la pregunta 1.

Parámetros obligatorios, opcionales y por defecto

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 = 4
pará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 es null o undefined.
  • Un parámetro opcional no puede ir antes de uno obligatorio: function f(a?: number, b: number) da error 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 undefined a mano para usar el valor por defecto (f(undefined, 2)). Por eso se ponen al final.

Funciones dentro de interfaces: métodos

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: () => void dice: "este objeto tiene una propiedad showHp que es una función sin parámetros y cuyo resultado no se usa".
  • Todo objeto declarado como Character está obligado a tener showHp. Si falta, tsc da error al crear el objeto, y por eso geralt.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.

Funciones que modifican objetos y early return

const healCharacter = (character: Character, amount: number) => {
  if (character.hp === 100) { return }
  character.hp += amount
}
  • El tipo atrapa errores de tipeo. character.healtPoints += amount da error porque healtPoints no existe en Character. En JavaScript esa línea crearía una propiedad nueva con NaN y 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. healCharacter no devuelve nada: modifica el mismo objeto geralt que 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.

Detalle de este proyecto: export { }

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.

Comparación con Angular

En Angular casi todo son métodos de clase y funciones flecha:

  • Los métodos de un componente usan this para leer su estado. Si pasas un método como callback (setTimeout(this.save, 0)), pierde su this; 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.

Errores comunes

  • Usar || en lugar de ?? para valores por defecto: secondNumber || 1 convierte un 0 legítimo en 1, y multiply(2, 0) daría 4 en lugar de 0.
  • Pensar que un parámetro opcional ya tiene valor. Dentro de la función es number | undefined y hay que manejar el undefined.
  • Escribir una flecha con llaves y olvidar el return: (a, b) => { a + b } devuelve undefined.
  • Definir un método de objeto como flecha (showHp: () => console.log(this.name)): this no es el objeto.
  • Comparar con === contra un límite (hp === 100) cuando lo que se quiere es un tope: usa >= y Math.min.

Resumen

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.

Preguntas de entrevista (senior)

1. ¿Qué diferencias hay entre una función flecha y una función tradicional, además de la sintaxis?

Respuesta:

  • this: la flecha toma el this del 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 su this y una flecha no.
  • arguments: la flecha no tiene su propio objeto arguments; se usan parámetros rest (...args).
  • new: una flecha no se puede usar como constructor y no tiene prototype.
  • Hoisting: una declaración function se puede llamar antes de la línea donde está escrita; una flecha asignada a const no.

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.

2. ¿Por qué const cb: () => void = () => 42 compila si la función devuelve un número?

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 vale number | undefined.
  • secondNumber: number | undefined: el que llama debe pasarlo, aunque sea undefined explícito.
  • secondNumber = 1: el que llama puede omitirlo y dentro siempre es number. 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.

4. ¿Cuándo anotarías el tipo de retorno y cuándo dejarías que se infiera?

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.ts sin revisar todo el programa.

En funciones internas cortas y callbacks, la inferencia basta.

5. ¿Qué son las sobrecargas de funciones y cuándo preferirías una unión?

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.

6. ¿Cómo se determina this en JavaScript y qué hace TypeScript para ayudar?

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(): this es geralt.
  • const fn = geralt.showHp; fn(): this es undefined (en módulos y modo estricto), y this.name falla en runtime.
  • fn.call(otro) o fn.bind(otro): this es otro.
  • En una flecha, this es 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.

7. En una interfaz, ¿qué diferencia hay entre showHp: () => void y showHp(): void?

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 runtime

onlyString 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.

8. ¿Es mejor que healCharacter modifique el objeto o que devuelva uno nuevo?

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.


Referencias del repositorio

  • 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.

Clone this wiki locally