¿Qué pasará cuando se ejecute este código?
let x: number = 5;
let y: string = x;TypeScript no permite asignar un valor de tipo number a una variable de tipo string. Es un error de compilación detectado por el sistema de tipos.
Todas las preguntas con sus respuestas correctas y explicaciones.
let x: number = 5;
let y: string = x;TypeScript no permite asignar un valor de tipo number a una variable de tipo string. Es un error de compilación detectado por el sistema de tipos.
? junto a age?interface User {
name: string;
age?: number;
}
const user: User = { name: "Alice" };El ? después de una propiedad en un interface la marca como opcional. El objeto no necesita incluirla.
type Status = "active" | "inactive" | "pending";
const s: Status = "deleted";Un union type restringe los valores permitidos. 'deleted' no forma parte del tipo Status, así que el compilador reportará un error.
identity<string>(42)?function identity<T>(arg: T): T {
return arg;
}
const result = identity<string>(42);El generic T se fijó explícitamente como string, pero el argumento 42 es de tipo number. TypeScript reportará un error de compilación.
console.log(Direction.Up)?enum Direction {
Up,
Down,
Left,
Right
}
console.log(Direction.Up);En un enum numérico sin valores explícitos, el primer miembro tiene valor 0 y los siguientes incrementan en 1.
push?const arr: readonly number[] = [1, 2, 3];
arr.push(4);Un array readonly no tiene métodos mutadores como push, pop, splice. Es una protección a nivel de tipos.
& en el contexto de tipos?type Point = {
x: number;
y: number;
};
type Point3D = Point & { z: number };
const p: Point3D = { x: 1, y: 2, z: 3 };El operador & crea un intersection type que combina todas las propiedades de ambos tipos en uno.
void?function greet(name: string): void {
console.log(`Hello, ${name}`);
}void significa que la función no devuelve ningún valor. Se diferencia de never, que significa que la función nunca termina.
as const en este contexto?const obj = { name: "Alice", age: 30 } as const;as const crea una const assertion — todas las propiedades se vuelven readonly con tipos literales (p. ej., 'Alice' en vez de string).
type StringOrNumber = string | number;
function process(val: StringOrNumber) {
if (typeof val === "string") {
return val.toUpperCase();
}
return val.toFixed(2);
}Type narrowing es la técnica de estrechar el tipo dentro de un bloque condicional. TypeScript reconoce automáticamente el tipo tras un check con typeof.
Config.interface Config {
debug: boolean;
}
interface Config {
verbose: boolean;
}
const cfg: Config = { debug: true, verbose: false };Declaration Merging es una característica única de los interfaces — varias declaraciones con el mismo nombre se fusionan en un único tipo. Los type aliases (type) no soportan esto.
function handleInput(val: unknown) {
return val.toUpperCase();
}El tipo unknown es la alternativa segura a any. Antes de usar el valor debes estrecharlo, p. ej. con typeof val === 'string'. Diferencia clave: any desactiva las comprobaciones, unknown las fuerza.
val is string en el tipo de retorno?function isString(val: unknown): val is string {
return typeof val === "string";
}
function process(input: unknown) {
if (isString(input)) {
console.log(input.toUpperCase());
}
}Un type predicate (val is string) es un custom type guard. Cuando la función devuelve true, TypeScript estrecha automáticamente el tipo dentro del bloque condicional. Más potente que devolver un boolean normal.
let a: Object = "hello";
let b: object = "hello";
let c: {} = "hello";Object (con O mayúscula) y {} aceptan primitivos (string, number). object (minúscula) solo acepta tipos no primitivos (objetos, arrays, funciones). Un string es un primitivo, así que b da error.
new Animal()?abstract class Animal {
abstract speak(): string;
greet(): string {
return `I say: ${this.speak()}`;
}
}
class Dog extends Animal {
speak() { return "Woof!"; }
}
const a = new Animal();Una clase abstract es una clase base que no puede instanciarse. Obliga a las subclases a implementar los métodos abstractos. new Dog() funciona, new Animal() no.
class User {
constructor(
public name: string,
private password: string,
protected role: string
) {}
}
const u = new User("Alice", "secret", "admin");
console.log(u.name, u.password, u.role);public — accesible en todas partes. private — solo dentro de la clase. protected — dentro de la clase y sus subclases. Desde fuera solo se ve name.
const user = {
name: "Alice",
address: {
city: null as string | null,
},
};
const city = user.address?.city ?? "Unknown";
console.log(city);?. (optional chaining) accede de forma segura a propiedades anidadas. ?? (nullish coalescing) devuelve el lado derecho solo cuando el izquierdo es null/undefined. city es null, así que el resultado es 'Unknown'.
! después de input y es seguro?function getLength(input: string | null): number {
return input!.length;
}El !. (non-null assertion) le dice al compilador: 'sé que esto no es null'. No genera código en runtime — si el valor realmente es null, obtendrás un runtime error. Es preferible usar type guards.
Pair y por qué wrong no compila?type Pair = [string, number];
const p: Pair = ["age", 30];
const [label, value] = p;
const wrong: Pair = [30, "age"];Una tupla es un array de longitud fija con tipos específicos en cada posición. [string, number] requiere un string en el índice 0 y un number en el índice 1. Invertir el orden es un error.
type e interface al extender tipos?interface User {
name: string;
}
type Admin = User & { role: string };
// vs
interface Manager extends User {
department: string;
}Interface se extiende con extends y soporta declaration merging (varias declaraciones). Type usa & (intersection) y maneja unions, tuplas y primitivos. Para APIs públicas se recomienda interface.
type Person = {
name: string
age: number
}
type Employee = {
name: string
email: string
}
type A = Person | Employee;
type B = Person & Employee;
const a: A = {
name: 'John',
age: 20,
email: 'john@test-email.co.uk',
}
const b: B = {
name: 'John',
age: 20,
email: 'john@test-email.co.uk',
}
console.log('A:', a.name, a.age, a.email)
console.log('B:', b.name, b.age, b.email)¡Es una trampa! Aunque el objeto a tenga todos los campos, el tipo A = Person | Employee significa 'o Person o Employee'. TypeScript no sabe qué variante tienes — podría ser Person (sin email) o Employee (sin age). Por eso a.age y a.email dan error sin narrowing. En cambio B = Person & Employee requiere TODOS los campos de ambos tipos, así que b.name, b.age y b.email están garantizados.
val en el último return?function formatValue(val: string | number | boolean) {
if (typeof val === "string") {
return val.toUpperCase();
}
if (typeof val === "number") {
return val.toFixed(2);
}
// Co wie TypeScript o val tutaj?
return val;
}TypeScript va estrechando progresivamente el tipo tras cada type guard. Después de typeof === 'string' elimina string, tras typeof === 'number' elimina number. Solo queda boolean. Es type narrowing automático — el compilador lo deduce solo.
res.data en el bloque success y res.message en el bloque error?type ApiResponse<T> =
| { status: "success"; data: T; timestamp: number }
| { status: "error"; message: string; code: number }
| { status: "loading" };
function handleResponse(res: ApiResponse<string[]>) {
if (res.status === "success") {
console.log(res.data.join(", "));
} else if (res.status === "error") {
console.log(res.message);
}
}status actúa como tag y permite a TypeScript estrechar el tipo en cada ramaUna discriminated union usa un campo común (status) para que TypeScript estreche el tipo automáticamente. En el bloque status === 'success' el compilador sabe que tienes la variante con data y timestamp. En error — message y code. Sin necesidad de casts.
ConfigKey y ConfigValue?const config = {
apiUrl: "https://api.example.com",
timeout: 5000,
debug: false,
} as const;
type Config = typeof config;
type ConfigKey = keyof Config;
type ConfigValue = Config[ConfigKey];Gracias a as const, los valores del objeto conservan tipos literales (no string/number/boolean). typeof config extrae el tipo del objeto. keyof da una unión de claves. Config[ConfigKey] es un indexed access type — la unión de TODOS los tipos de valor. Una combinación poderosa para configuración type-safe.
in como type guard?interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function move(animal: Bird | Fish) {
if ("fly" in animal) {
animal.fly();
} else {
animal.swim();
}
}if, TypeScript sabe que tenemos Bird'fly' in animal comprueba en runtime si el objeto tiene el método fly. TypeScript lo reconoce como type guard: en el bloque if sabe que tenemos Bird (solo Bird tiene fly). En else sabe que tenemos Fish. El operador in es alternativa a instanceof cuando trabajas con interfaces.
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // ?
type B = IsString<number>; // ?Los conditional types funcionan como un ternario: T extends string comprueba si T extiende string. Para string → 'yes', para number → 'no'.
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K];
};DeepReadonly recorre recursivamente todas las propiedades del objeto. Si una propiedad es un objeto, se aplica de nuevo. Resultado: todo el objeto se vuelve inmutable.
Result?type ExtractReturn<T> = T extends (...args: any[]) => infer R
? R
: never;
type Result = ExtractReturn<() => Promise<string>>;La palabra clave infer R extrae el tipo de retorno de la función. La función devuelve Promise<string>, así que R = Promise<string>.
ClickEvent?type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">;Los template literal types permiten crear nuevos tipos string. Capitalize pone en mayúscula la primera letra: 'click' → 'Click', resultado 'onClick'.
type Flatten<T> = T extends Array<infer U> ? U : T;
type A = Flatten<string[]>;
type B = Flatten<number>;Flatten extrae el tipo de elemento de un array usando infer. string[] → string. Para number (no es un array) devuelve el propio tipo.
Result?type Exclude<T, U> = T extends U ? never : T;
type Result = Exclude<"a" | "b" | "c", "a" | "c">;Exclude trabaja distributivamente sobre union types. Para cada miembro: si extiende U → never (eliminado); si no, se mantiene. Solo queda 'b'.
UserGetters?type Getters<T> = {
[K in keyof T as `get${Capitalize<K & string>}`]: () => T[K];
};
type UserGetters = Getters<{ name: string; age: number }>;El key remapping (as) en mapped types permite renombrar claves. Aquí cada clave K se transforma en get${Capitalize<K>} y el valor pasa a ser una función getter.
assertNever?function assertNever(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
type Shape = "circle" | "square";
function area(shape: Shape) {
switch (shape) {
case "circle": return Math.PI;
case "square": return 1;
default: return assertNever(shape);
}
}assertNever recibe never — un tipo que no debería existir nunca. Si añades una nueva variante a Shape y no la manejas en el switch, el compilador reportará un error.
Result?type UnionToIntersection<U> =
(U extends any ? (x: U) => void : never) extends
(x: infer I) => void ? I : never;
type Result = UnionToIntersection<{ a: 1 } | { b: 2 }>;UnionToIntersection convierte una unión en intersección aprovechando la contravarianza de la posición de parámetro de función. Resultado: { a: 1 } & { b: 2 }.
payUSD(euros)?declare const brand: unique symbol;
type Branded<T, B> = T & { [brand]: B };
type USD = Branded<number, "USD">;
type EUR = Branded<number, "EUR">;
function payUSD(amount: USD) { /* ... */ }
const euros = 100 as EUR;
payUSD(euros);Los branded types (tipos nominales) añaden un 'phantom brand' a un tipo estructural. USD y EUR son tipos distintos aunque ambos se basen en number.
-? y -readonly en mapped types?type MakeRequired<T> = {
[P in keyof T]-?: T[P];
};
type MakeMutable<T> = {
-readonly [P in keyof T]: T[P];
};
interface Config {
readonly host?: string;
readonly port?: number;
}
type WritableConfig = MakeMutable<MakeRequired<Config>>;El prefijo - en mapped types elimina un modificador: -? hace el campo requerido (como Required<T>), -readonly lo hace mutable. WritableConfig acaba siendo { host: string; port: number }.
UserPreview y UserWithoutEmail?interface User {
id: number;
name: string;
email: string;
age: number;
}
type UserPreview = Pick<User, "id" | "name">;
type UserWithoutEmail = Omit<User, "email">;Pick<T, K> selecciona las claves indicadas de un tipo. Omit<T, K> elimina las claves indicadas. Ambos crean un nuevo tipo con un subconjunto de las propiedades del original.
Record<Roles, boolean> y qué pasaría si omitiéramos la clave viewer?type Roles = "admin" | "editor" | "viewer";
type Permissions = Record<Roles, boolean>;
const perms: Permissions = {
admin: true,
editor: true,
viewer: false,
};Record<K, V> crea un tipo con claves K y valores V. Todas las claves son obligatorias — omitir cualquiera es un error de compilación.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rect"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rect":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
}
}kind compartido permite a TypeScript estrechar el tipo en cada ramaUna Discriminated Union usa un campo tag (discriminante) como kind. TypeScript estrecha automáticamente el tipo tras chequear el tag en switch/if — dándote acceso a los campos específicos de la variante.
function logLength<T extends { length: number }>(obj: T): void {
console.log(obj.length);
}
logLength("hello");
logLength([1, 2, 3]);
logLength(42);extends { length: number } la exigeEl generic constraint T extends { length: number } exige que T tenga una propiedad length. String y array tienen length, así que pasan. Number no — error de compilación.
Result?function getParams<T extends (...args: any[]) => any>(
fn: T
): Parameters<T> {
return [] as any;
}
type Result = Parameters<(a: string, b: number) => void>;Parameters<T> extrae los tipos de los parámetros de una función como una tupla. Para (a: string, b: number) => void devuelve [string, number].
Result?type NonStringKeys<T> = {
[K in keyof T]: T[K] extends string ? never : K;
}[keyof T];
interface User {
id: number;
name: string;
age: number;
email: string;
}
type Result = NonStringKeys<User>;Este tipo filtra claves: si el valor es string → never (eliminada); si no, mantiene el nombre de la clave. id y age son number, así que se quedan. name y email (string) se eliminan.
Numbers?type Extract<T, U> = T extends U ? T : never;
type Numbers = Extract<string | number | boolean, number | boolean>;Extract<T, U> es lo opuesto a Exclude — mantiene de la unión T solo los tipos asignables a U. string no está en U, se elimina. Quedan number | boolean.
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true
}
}
function add(a, b) {
return a + b;
}strict: true activa entre otras noImplicitAny. Los parámetros a y b no tienen tipos y no pueden inferirse del contexto — el compilador les daría any por defecto, pero la flag lo impide.
type ReadOnly<T> = { readonly [P in keyof T]: T[P] };
type MyPartial<T> = { [P in keyof T]?: T[P] };
interface Todo {
title: string;
done: boolean;
}
type A = ReadOnly<Todo>;
type B = MyPartial<Todo>;Los mapped types iteran sobre las claves de un tipo y permiten añadir modificadores. ReadOnly añade readonly a cada campo, MyPartial añade ? (opcionalidad). Así funcionan los Readonly<T> y Partial<T> integrados.
CSSSpacing y cómo se generan?type CSSProperty = "margin" | "padding";
type Direction = "top" | "right" | "bottom" | "left";
type CSSSpacing = {
[K in `${CSSProperty}-${Direction}`]: string;
};
const spacing: CSSSpacing = {
"margin-top": "10px",
"margin-right": "20px",
// ... wymaga wszystkich 8 kombinacji
};Un template literal type dentro de un mapped type crea TODAS las combinaciones: margin-top, margin-right, margin-bottom, margin-left, padding-top, padding-right, padding-bottom, padding-left = 8 campos. TypeScript genera el producto cartesiano de strings. Una herramienta potente para generar tipos CSS-in-JS.
type Awaited<T> =
T extends Promise<infer U>
? Awaited<U>
: T;
type A = Awaited<Promise<string>>;
type B = Awaited<Promise<Promise<number>>>;
type C = Awaited<boolean>;Awaited 'desempaqueta' Promises de forma recursiva: Promise<string> → string, Promise<Promise<number>> → Promise<number> → number. Si T no es Promise, devuelve T sin cambios (boolean). Es una versión simplificada del Awaited<T> integrado en TypeScript 4.5+.
type ToArray<T> = T extends any ? T[] : never;
type A = ToArray<string | number>;
type B = ToArray<string | number | boolean>;¡Los conditional types con parámetro T se distribuyen sobre las uniones! TypeScript aplica la condición a CADA miembro de la unión por separado: string → string[], number → number[]. El resultado es una unión de arrays, no un array de uniones. Para desactivar la distribución, envuelve T en [T].
r y g, y en qué se diferencia satisfies de : Colors?type Colors = Record<string, [number, number, number] | string>;
const palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255],
} satisfies Colors;
const r = palette.red; // jaki typ?
const g = palette.green; // jaki typ?El operador satisfies (TS 4.9+) comprueba si un valor es conforme con un tipo, pero NO ensancha el tipo de la variable. Con : Colors, ambos campos tendrían tipo [number,number,number] | string. Con satisfies, TypeScript conserva los tipos estrechos — red es una tupla, green es un string.
StringFields y NumberFields?type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface Model {
id: number;
name: string;
active: boolean;
email: string;
age: number;
}
type StringFields = PickByType<Model, string>;
type NumberFields = PickByType<Model, number>;El key remapping con as + un conditional type permite filtrar claves. Cuando T[K] extends U es false, la clave se mapea a never — eliminándola del resultado. Resultado: StringFields tiene los campos cuyo valor es string, NumberFields — number.
console.log y cómo se llama esta técnica?const add = (a: number) => (b: number) => a + b;
const add5 = add(5);
console.log(add5(3));Currying transforma una función de varios argumentos en una secuencia de funciones de un solo argumento. add(5) devuelve (b) => 5 + b, así que add5(3) = 8.
transform(5)?const pipe = <T>(...fns: Array<(arg: T) => T>) =>
(value: T): T =>
fns.reduce((acc, fn) => fn(acc), value);
const transform = pipe(
(x: number) => x * 2,
(x: number) => x + 1,
(x: number) => x * 3
);
console.log(transform(5));Pipe ejecuta las funciones de izquierda a derecha: 5 → *2 = 10 → +1 = 11 → *3 = 33.
type Option<T> = { tag: "some"; value: T } | { tag: "none" };
const map = <A, B>(opt: Option<A>, fn: (a: A) => B): Option<B> =>
opt.tag === "some"
? { tag: "some", value: fn(opt.value) }
: { tag: "none" };
const result = map({ tag: "some", value: 5 }, x => x * 2);El monad Option (Maybe) envuelve un valor que podría no existir. La función map es una operación de functor — transforma el valor dentro del contenedor sin cambiar su estructura.
const compose = <A, B, C>(
f: (b: B) => C,
g: (a: A) => B
) => (a: A): C => f(g(a));
const double = (x: number) => x * 2;
const toString = (x: number) => `Value: ${x}`;
const doubleThenString = compose(toString, double);
console.log(doubleThenString(5));Compose ejecuta funciones de derecha a izquierda (composición matemática f∘g). Pipe hace lo contrario — de izquierda a derecha.
flatMap en el contexto de monads?type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };
const flatMap = <T, E, U>(
result: Result<T, E>,
fn: (value: T) => Result<U, E>
): Result<U, E> =>
result.ok ? fn(result.value) : result;
const parse = (s: string): Result<number, string> => {
const n = Number(s);
return isNaN(n)
? { ok: false, error: "Not a number" }
: { ok: true, value: n };
};flatMap (bind/chain) es la operación monádica fundamental. Se diferencia de map en que la función devuelve un nuevo contenedor (Result), y flatMap 'aplana' el resultado.
const memoize = <T extends (...args: any[]) => any>(fn: T): T => {
const cache = new Map<string, ReturnType<T>>();
return ((...args: Parameters<T>): ReturnType<T> => {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key)!;
const result = fn(...args);
cache.set(key, result);
return result;
}) as T;
};
const factorial = memoize((n: number): number =>
n <= 1 ? 1 : n * factorial(n - 1)
);La memoización es una técnica que recuerda los resultados de una función para argumentos dados. Las llamadas siguientes con los mismos argumentos devuelven resultados cacheados en lugar de volver a calcular.
updated?type Lens<S, A> = {
get: (s: S) => A;
set: (a: A, s: S) => S;
};
const nameLens: Lens<{ name: string; age: number }, string> = {
get: (s) => s.name,
set: (a, s) => ({ ...s, name: a }),
};
const user = { name: "Alice", age: 30 };
const updated = nameLens.set("Bob", user);Lens es un patrón de programación funcional para actualizaciones inmutables de estructuras anidadas. Set crea un nuevo objeto con el campo actualizado, conservando el resto.
type Predicate<T> = (value: T) => boolean;
const and = <T>(...preds: Predicate<T>[]): Predicate<T> =>
(value) => preds.every(p => p(value));
const or = <T>(...preds: Predicate<T>[]): Predicate<T> =>
(value) => preds.some(p => p(value));
const not = <T>(pred: Predicate<T>): Predicate<T> =>
(value) => !pred(value);
const isEven: Predicate<number> = n => n % 2 === 0;
const isPositive: Predicate<number> = n => n > 0;
const isOddAndPositive = and(not(isEven), isPositive);
console.log(isOddAndPositive(3), isOddAndPositive(-3));Composición de predicados: 3 es impar (not even) y positivo → true. -3 es impar pero no positivo → false.
Task?type Task<T> = {
fork: (reject: (e: Error) => void, resolve: (t: T) => void) => void;
};
const taskOf = <T>(value: T): Task<T> => ({
fork: (_, resolve) => resolve(value),
});
const taskMap = <A, B>(task: Task<A>, fn: (a: A) => B): Task<B> => ({
fork: (reject, resolve) =>
task.fork(reject, (a) => resolve(fn(a))),
});Task es un monad asíncrono lazy. A diferencia de Promise, no se ejecuta de inmediato — el cómputo arranca solo cuando se llama a fork().
const trampoline = <T>(fn: () => T | (() => T)): T => {
let result = fn();
while (typeof result === "function") {
result = (result as () => T)();
}
return result;
};
const sumTo = (n: number, acc: number = 0): any =>
n === 0 ? acc : () => sumTo(n - 1, acc + n);
console.log(trampoline(() => sumTo(100000)));Trampoline convierte la recursión de cola en un bucle while, evitando el stack overflow. En lugar de llamarse recursivamente, devuelve un thunk (función) que se ejecuta en el bucle.
const double = (x: number): number => x * 2;
const result1 = double(5);
const result2 = double(5);
let counter = 0;
const impureDouble = (x: number): number => {
counter++;
return x * 2;
};Una pure function cumple dos condiciones: (1) mismo input → mismo output, (2) cero side effects. impureDouble muta la variable externa counter, así que es impure. Referential transparency significa que double(5) siempre se puede sustituir por el valor 10.
withLogging y cómo se llama este tipo de función?const withLogging = <T extends (...args: any[]) => any>(fn: T) =>
(...args: Parameters<T>): ReturnType<T> => {
console.log("Calling with:", args);
const result = fn(...args);
console.log("Result:", result);
return result;
};
const add = (a: number, b: number) => a + b;
const loggedAdd = withLogging(add);
loggedAdd(2, 3);Una Higher-Order Function (HOF) es una función que toma o devuelve otra función. withLogging toma la función fn y devuelve una nueva con logging añadido — como un decorator. En FP las funciones son ciudadanos de primera clase.
function createCounter() {
let count = 0;
return {
increment: () => ++count,
getCount: () => count,
};
}
const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getCount());Un closure es un mecanismo por el que una función conserva acceso a las variables del scope en el que fue definida — incluso después de que ese scope haya terminado. count vive en el closure de createCounter.
result?type Either<L, R> =
| { tag: "left"; value: L }
| { tag: "right"; value: R };
const left = <L>(value: L): Either<L, never> =>
({ tag: "left", value });
const right = <R>(value: R): Either<never, R> =>
({ tag: "right", value });
const map = <L, A, B>(
either: Either<L, A>,
fn: (a: A) => B
): Either<L, B> =>
either.tag === "right"
? right(fn(either.value))
: either;
const result = map(right(10), x => x * 2);Either es un monad con dos variantes: Left (convencionalmente error) y Right (éxito). La función map transforma el valor solo en Right, dejando Left tal cual — alternativa a try/catch.
type Thunk<T> = () => T;
const lazyValue: Thunk<number> = () => {
console.log("Computing...");
return 42;
};
// Wartość nie jest jeszcze obliczona
console.log("Before");
const result = lazyValue();
console.log("After:", result);Un Thunk envuelve un efecto/cómputo en una función para ejecución diferida (lazy evaluation). El cómputo no arranca hasta que llamas al thunk — te da control sobre cuándo se ejecuta.
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Podejście imperatywne
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 === 0) {
sum += numbers[i] * 2;
}
}
// Podejście deklaratywne
const result = numbers
.filter(n => n % 2 === 0)
.map(n => n * 2)
.reduce((acc, n) => acc + n, 0);El estilo declarativo (FP) dice 'qué' hacer: filtra los pares, duplica, suma. El imperativo dice 'cómo': itera, comprueba, suma. El declarativo es más legible y menos propenso a errores.
safeDivide (partial) y totalDivide (total)?const safeDivide = (a: number, b: number): number | never => {
if (b === 0) throw new Error("Division by zero");
return a / b;
};
const totalDivide = (a: number, b: number): number =>
b === 0 ? 0 : a / b;Una total function está definida para todo input dentro de su dominio — siempre devuelve un resultado. Una partial function puede lanzar excepción (como safeDivide para b=0). En FP preferimos total functions.
const idempotent = (arr: number[]): number[] =>
[...new Set(arr)].sort((a, b) => a - b);
console.log(idempotent([3, 1, 2, 1, 3]));
console.log(idempotent(idempotent([3, 1, 2, 1, 3])));Idempotencia: f(f(x)) === f(x). La primera llamada deduplica y ordena el array. Una segunda llamada sobre el resultado ya procesado da idéntico resultado. Ejemplos: Math.abs, Array.sort sobre un array ya ordenado.
result? ¿Qué métodos de array son clave en FP y por qué?const users = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
{ name: "Charlie", age: 35 },
];
const result = users
.filter(u => u.age >= 30)
.map(u => u.name)
.reduce((acc, name) => acc + ", " + name);filter (selecciona), map (transforma) y reduce (combina) son operaciones FP fundamentales sobre colecciones. Son declarativas, no mutan el origen y se pueden encadenar en pipeline. Resultado: Alice y Charlie tienen >= 30 años.
result y fallback?const Maybe = <T>(value: T | null | undefined) => ({
map: <U>(fn: (val: T) => U) =>
value != null ? Maybe(fn(value)) : Maybe<U>(null),
getOrElse: (defaultVal: T) =>
value != null ? value : defaultVal,
});
const result = Maybe("hello")
.map(s => s.toUpperCase())
.map(s => s + "!")
.getOrElse("default");
const fallback = Maybe<string>(null)
.map(s => s.toUpperCase())
.getOrElse("default");Maybe maneja null/undefined: si el valor existe, map sigue transformándolo. Si es null — toda la cadena se omite y getOrElse devuelve el valor por defecto. Una alternativa a las cadenas de checks if-null.
result y por qué este estilo se prefiere en FP?const users = [
{ name: "Alice", age: 30, role: "admin" },
{ name: "Bob", age: 25, role: "user" },
{ name: "Charlie", age: 35, role: "admin" },
{ name: "Diana", age: 28, role: "user" },
] as const;
const result = users
.filter(u => u.role === "admin")
.map(u => ({ ...u, name: u.name.toUpperCase() }))
.sort((a, b) => a.age - b.age);El pipeline filter→map→sort es la esencia del FP: cada operación crea una nueva estructura, el original queda intacto. El estilo declarativo dice QUÉ haces (filtra los admins, pasa los nombres a uppercase, ordena por edad), no CÓMO. Fácil de leer, testear y debuggear.
type Fn<A, B> = (a: A) => B;
const pipe2 = <A, B, C>(
f: Fn<A, B>,
g: Fn<B, C>
): Fn<A, C> => (a) => g(f(a));
const parse = (s: string): number => parseInt(s, 10);
const double = (n: number): number => n * 2;
const format = (n: number): string => `Result: ${n}`;
const transform = pipe2(pipe2(parse, double), format);
console.log(transform("21"));pipe2 compone dos funciones: f: A→B y g: B→C dando A→C. Los generics garantizan que los tipos coincidan — no se puede combinar string→number con boolean→string porque B debe coincidir. parse('21')=21, double(21)=42, format(42)='Result: 42'.
Tree<T>?type Tree<T> =
| { type: "leaf"; value: T }
| { type: "node"; left: Tree<T>; right: Tree<T> };
const sumTree = (tree: Tree<number>): number =>
tree.type === "leaf"
? tree.value
: sumTree(tree.left) + sumTree(tree.right);
const myTree: Tree<number> = {
type: "node",
left: { type: "leaf", value: 1 },
right: {
type: "node",
left: { type: "leaf", value: 2 },
right: { type: "leaf", value: 3 },
},
};
console.log(sumTree(myTree));Tree<T> es un algebraic data type (ADT) — una discriminated union con definición recursiva. sumTree suma de forma recursiva: node(leaf(1), node(leaf(2), leaf(3))) = 1 + (2 + 3) = 6. Los ADT son base del FP — modelan estructuras de datos sin clases, puramente con tipos.
const map = <A, B>(arr: A[], fn: (a: A) => B): B[] =>
arr.map(fn);
const identity = <T>(x: T): T => x;
const double = (x: number) => x * 2;
const addOne = (x: number) => x + 1;
// Law 1: Identity
map([1, 2, 3], identity) // === [1, 2, 3]
// Law 2: Composition
map(map([1, 2, 3], double), addOne)
// ===
map([1, 2, 3], x => addOne(double(x)))Un functor es un contenedor (Array, Maybe, Promise) con una operación map que cumple dos leyes: (1) mapear identidad no cambia el contenedor, (2) mapear dos funciones en secuencia da el mismo resultado que mapear su composición. Esto garantiza un comportamiento predecible.
result y qué es un transducer?type Reducer<A, B> = (acc: B, val: A) => B;
type Transducer<A, B> = <C>(rf: Reducer<B, C>) => Reducer<A, C>;
const mapT = <A, B>(fn: (a: A) => B): Transducer<A, B> =>
(rf) => (acc, val) => rf(acc, fn(val));
const filterT = <A>(pred: (a: A) => boolean): Transducer<A, A> =>
(rf) => (acc, val) => pred(val) ? rf(acc, val) : acc;
const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const isEven = (n: number) => n % 2 === 0;
const triple = (n: number) => n * 3;
const xform = (rf: Reducer<number, number[]>) =>
filterT(isEven)(mapT(triple)(rf));
const result = nums.reduce(
xform((acc, val) => [...acc, val]),
[] as number[]
);Un transducer combina filter y map en una sola pasada — sin crear arrays intermedios como un chain .filter().map(). Filtra los pares (2,4,6,8,10) y multiplica ×3 en una única reducción. Más eficiente que los chains sobre conjuntos grandes.
interface ButtonProps {
label: string;
onClick: () => void;
variant?: "primary" | "secondary";
}
const Button = ({ label, onClick, variant = "primary" }: ButtonProps) => (
<button onClick={onClick} className={variant}>
{label}
</button>
);La mejor práctica es definir un interface/type para los props y usarlo como tipo del parámetro. Los props opcionales se marcan con ? y los valores por defecto se ponen en la destructuración.
setCount('hello') provocará un error?const [count, setCount] = useState(0);
setCount("hello");TypeScript infiere el tipo del state desde el valor inicial. useState(0) da el tipo number, así que setCount solo acepta number. Para union types usa useState<string | number>(0).
useState<User | null>(null)?const [user, setUser] = useState<User | null>(null);
if (user) {
console.log(user.name);
}null — no sabría nada de UserCuando el valor inicial es null, TypeScript infiere el tipo como null. El generic explícito <User | null> dice: el state puede ser User o null. Tras if (user), TypeScript estrecha el tipo a User.
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.currentTarget.value);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};React proporciona sus propios tipos de eventos (React.MouseEvent, React.ChangeEvent, React.FormEvent, etc.) con un generic para el elemento HTML. Esto te da acceso completo a las propiedades tipadas del elemento vía currentTarget.
useRef<HTMLInputElement>(null) necesita un generic?const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} />;useRef<T>(null) crea un ref con tipo T | null. El generic le dice a TypeScript que .current será HTMLInputElement (o null antes del mount). Así obtienes autocomplete completo del DOM API.
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map((item, i) => <li key={i}>{renderItem(item)}</li>)}</ul>;
}
<List items={["a", "b"]} renderItem={(item) => <span>{item}</span>} />Un generic component usa el parámetro de tipo <T> tanto en la interface de props como en el componente. TypeScript infiere T de los datos pasados — aquí string[] → T = string. El renderizado está completamente tipado.
ThemeContextType | undefined?interface ThemeContextType {
theme: "light" | "dark";
toggle: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used within ThemeProvider");
return ctx;
}createContext necesita un valor por defecto. Undefined + custom hook con guard es un patrón seguro: si el componente no está dentro del provider, el hook lanza un error legible en lugar de devolver undefined.
children y en qué se diferencia de React.ReactElement?interface CardProps {
title: string;
children: React.ReactNode;
}
const Card = ({ title, children }: CardProps) => (
<div>
<h2>{title}</h2>
<div>{children}</div>
</div>
);React.ReactNode es el tipo más amplio para children: JSX, string, number, boolean, null, undefined, arrays. React.ReactElement es estrictamente un elemento JSX (<Component /> o <div />). Para children casi siempre usa ReactNode.
React.ComponentPropsWithoutRef<'button'>?type ButtonProps = React.ComponentPropsWithoutRef<"button"> & {
variant?: "primary" | "danger";
};
const Button = ({ variant = "primary", children, ...rest }: ButtonProps) => (
<button className={variant} {...rest}>
{children}
</button>
);ComponentPropsWithoutRef<'element'> extrae el conjunto completo de props de un elemento HTML nativo. Combinarlo con & permite añadir props propios (variant). Patrón ideal para crear componentes UI reusables.
React.memo con TypeScript?const MemoizedList = React.memo(function UserList({
users,
onSelect,
}: {
users: User[];
onSelect: (user: User) => void;
}) {
return (
<ul>
{users.map((u) => (
<li key={u.id} onClick={() => onSelect(u)}>{u.name}</li>
))}
</ul>
);
});React.memo() es genérico y automáticamente infiere los tipos de props del componente envuelto. TypeScript garantiza que la comparación de props (shallow comparison) sea type-safe.
dispatch de useReducer?type Action =
| { type: "increment" }
| { type: "decrement" }
| { type: "set"; payload: number };
function reducer(state: number, action: Action): number {
switch (action.type) {
case "increment": return state + 1;
case "decrement": return state - 1;
case "set": return action.payload;
}
}
const [count, dispatch] = useReducer(reducer, 0);useReducer infiere el tipo de dispatch del tipo Action. La discriminated union (campo type) garantiza que dispatch solo acepta acciones válidas. action.payload solo está disponible en el case 'set'.
useFetch usa el generic <T,>?const useFetch = <T,>(url: string) => {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetch(url)
.then((res) => res.json())
.then((json: T) => setData(json))
.catch((err) => setError(err))
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
};
const { data } = useFetch<User[]>("/api/users");En archivos .tsx, <T> se interpretaría como un tag JSX. <T,> es el workaround — la coma final distingue un generic de un tag. El caller escribe useFetch<User[]>(url) y data tiene tipo User[] | null.
React.forwardRef?const ForwardedInput = React.forwardRef<
HTMLInputElement,
{ label: string }
>(({ label, ...props }, ref) => (
<div>
<label>{label}</label>
<input ref={ref} {...props} />
</div>
));React.forwardRef<RefType, PropsType> — el primer generic es el tipo del elemento DOM (ref), el segundo el tipo de los props. Permite a un componente hijo reenviar una ref a un elemento interno.
React.ReactNode y React.ReactElement en los props?type PropsWithChildren<P = {}> = P & {
children?: React.ReactNode;
};
interface LayoutProps {
sidebar: React.ReactElement;
}
const Layout = ({ sidebar, children }: PropsWithChildren<LayoutProps>) => (
<div>
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
);Usa ReactNode para children (acepta de todo). Usa ReactElement cuando exijas un componente JSX concreto (p. ej. sidebar debe ser un elemento, no texto). Es una distinción importante al tipar layouts.
AsyncState<T> es mejor que State<T> para gestionar estado asíncrono?type Status = "idle" | "loading" | "success" | "error";
interface State<T> {
status: Status;
data: T | null;
error: string | null;
}
type AsyncState<T> =
| { status: "idle"; data: null; error: null }
| { status: "loading"; data: null; error: null }
| { status: "success"; data: T; error: null }
| { status: "error"; data: null; error: string };Una discriminated union sobre el status elimina estados imposibles. TypeScript sabe: success → data: T (no null), error → error: string (no null). State<T> permite error + data al mismo tiempo — un bug esperando ocurrir.
useCallback y por qué hace falta as string en formData.get?const handleSubmit = useCallback(
async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const email = formData.get("email") as string;
await submitForm({ email });
},
[]
);FormData.get() devuelve string | File | null porque los formularios pueden contener archivos. Cuando sabes que el campo es texto, as string estrecha el tipo. Alternativa: usar un type guard typeof val === 'string'.
| undefined a diferencia del ejemplo anterior?type Theme = "light" | "dark";
const ThemeContext = createContext<Theme>("light");
const useTheme = () => useContext(ThemeContext);
const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
const [theme, setTheme] = useState<Theme>("light");
return (
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
);
};Cuando createContext recibe un valor por defecto razonable ("light"), el componente lo obtiene incluso sin provider. No hace falta undefined ni guard. El patrón con undefined es necesario cuando NO HAY un default razonable.
type ButtonVariant = "primary" | "secondary" | "danger";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
isLoading?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = "primary", isLoading, children, disabled, ...props }, ref) => (
<button
ref={ref}
disabled={disabled || isLoading}
className={variant}
{...props}
>
{isLoading ? "Loading..." : children}
</button>
)
);El patrón extends HTMLAttributes es estándar en design systems. El componente acepta todos los props nativos de <button> (onClick, disabled, type, etc.) + los propios (variant, isLoading). forwardRef permite reenviar la ref.
const useLocalStorage = <T,>(
key: string,
initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] => {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value: T | ((prev: T) => T)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
};
return [storedValue, setValue];
};El hook imita la API de useState — devuelve una tupla [value, setter]. El union type T | ((prev: T) => T) permite usar el setter con un valor o con un callback. Es el mismo patrón que el setState integrado.
keyof T en el tipo Column<T>?type Column<T> = {
key: keyof T;
header: string;
render?: (value: T[keyof T], row: T) => React.ReactNode;
};
interface TableProps<T> {
data: T[];
columns: Column<T>[];
}
function Table<T extends Record<string, unknown>>({ data, columns }: TableProps<T>) {
return (
<table>
<thead>
<tr>{columns.map(col => <th key={String(col.key)}>{col.header}</th>)}</tr>
</thead>
<tbody>
{data.map((row, i) => (
<tr key={i}>
{columns.map(col => (
<td key={String(col.key)}>
{col.render ? col.render(row[col.key], row) : String(row[col.key])}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}key en una columna sea una clave real del tipo T — no se puede indicar un campo inexistente. TypeScript autocompleta las claves disponibleskeyof T en un componente Table genérico garantiza type-safety: las columnas solo pueden referenciar campos existentes en los datos. Añadir un nuevo campo a los datos lo deja automáticamente disponible en las columnas.
type PolymorphicProps<E extends React.ElementType> = {
as?: E;
children: React.ReactNode;
} & Omit<React.ComponentPropsWithoutRef<E>, "as" | "children">;
function Box<E extends React.ElementType = "div">({
as,
children,
...props
}: PolymorphicProps<E>) {
const Component = as || "div";
return <Component {...props}>{children}</Component>;
}
<Box as="a" href="/about">Link</Box>
<Box as="button" onClick={() => {}}>Click</Box>
<Box>Default div</Box>as para cambiar el tag HTML — TypeScript tipa automáticamente los props según el elemento elegidoUna polymorphic component (patrón popular en Chakra UI, Radix) permite cambiar el elemento renderizado vía el prop as. Gracias a los generics, TypeScript sabe: si as='a', los props incluyen href. Si as='button' — incluyen onClick. Type-safety completa.
{ data: T | null; loading: boolean; error: Error | null }?type QueryResult<T> =
| { status: "idle"; data: undefined; error: undefined }
| { status: "loading"; data: undefined; error: undefined }
| { status: "success"; data: T; error: undefined }
| { status: "error"; data: undefined; error: Error };
function useQuery<T>(url: string): QueryResult<T> {
// implementation
}
const result = useQuery<User[]>("/api/users");
if (result.status === "success") {
result.data.map(u => u.name); // TypeScript wie!
}Con un simple { data, loading, error } nada impide estados imposibles como loading: true, data: someData, error: someError. Una discriminated union los excluye a nivel de tipos. Tras status === 'success', TypeScript garantiza que data es T (no undefined).
interface DataListProps<T> {
items: T[];
filterFn?: (item: T) => boolean;
sortFn?: (a: T, b: T) => number;
children: (item: T, index: number) => React.ReactNode;
}
function DataList<T>({
items,
filterFn,
sortFn,
children,
}: DataListProps<T>) {
let processed = [...items];
if (filterFn) processed = processed.filter(filterFn);
if (sortFn) processed = processed.sort(sortFn);
return <>{processed.map((item, i) => children(item, i))}</>;
}
<DataList
items={users}
filterFn={u => u.active}
sortFn={(a, b) => a.name.localeCompare(b.name)}
>
{(user) => <UserCard key={user.id} user={user} />}
</DataList>DataList es un componente genérico con render prop pattern. TypeScript infiere T desde items y lo propaga a filterFn, sortFn y children. Separa la lógica (filtrado, ordenación) de la presentación (render prop). FP puro en React.
handleChange('name', 42) y handleChange('unknown', 'value') dan error?type FormFields = {
name: string;
email: string;
age: number;
};
const useForm = <T extends Record<string, unknown>>(initial: T) => {
const [values, setValues] = useState<T>(initial);
const handleChange = <K extends keyof T>(
field: K,
value: T[K]
) => {
setValues(prev => ({ ...prev, [field]: value }));
};
return { values, handleChange };
};
const { values, handleChange } = useForm<FormFields>({
name: "",
email: "",
age: 0,
});
handleChange("name", "Alice"); // OK
handleChange("age", 25); // OK
handleChange("name", 42); // Error!
handleChange("unknown", "value"); // Error!El generic K extends keyof T en handleChange crea una dependencia: field: K restringe a las claves existentes, value: T[K] exige el tipo correspondiente a esa clave. TypeScript chequea AMBOS a la vez — no se puede pasar un campo erróneo ni un tipo de valor erróneo.
interface DialogProps {
open: boolean;
onClose: () => void;
children: React.ReactNode;
}
interface DialogComposition {
Header: React.FC<{ children: React.ReactNode }>;
Body: React.FC<{ children: React.ReactNode }>;
Footer: React.FC<{ children: React.ReactNode }>;
}
const Dialog: React.FC<DialogProps> & DialogComposition = ({
open,
onClose,
children,
}) => {
if (!open) return null;
return <div className="dialog">{children}</div>;
};
Dialog.Header = ({ children }) => <header>{children}</header>;
Dialog.Body = ({ children }) => <main>{children}</main>;
Dialog.Footer = ({ children }) => <footer>{children}</footer>;El Compound Component pattern (popular en Radix, Headless UI) usa dot notation para composición. El tipo FC<DialogProps> & DialogComposition es una intersection — TypeScript sabe que Dialog es a la vez componente y tiene sub-componentes. Te da autocomplete sobre Dialog.