TypeScript desde cero para quien ya sabe JavaScript
Guía profunda de TypeScript: strict mode, interfaces, unions, genéricos, utility types, tipado de APIs, React/Nest y un plan de 14 días para dejar de usar any.
camilogaon
@camilogaon
TypeScript desde cero para quien ya sabe JavaScript
Si ya manejas JavaScript y sigues chocando con bugs tontos en producción (undefined is not a function, props mal tipadas, APIs que “a veces” traen null), TypeScript es la mejora más rentable que puedes hacer a tu stack.
Esta guía te lleva de JS cómodo a TS profesional: tipos, interfaces, genéricos, narrowing, utilidades y cómo usarlo en proyectos reales (Next.js / NestJS).
Prerrequisito: JavaScript intermedio (funciones, arrays, objetos, async/await).
Objetivo: escribir TS con confianza, no solo “poneranypara que compile”.
//1. Qué problema resuelve TypeScript
JavaScript es flexible. Esa flexibilidad es poder… y también fuente de errores silenciosos.
function getLength(value) {
return value.length;
}
getLength("hola"); // 4
getLength([1, 2]); // 2
getLength(10); // 💥 en runtime
Con TypeScript:
function getLength(value: string | unknown[]) {
return value.length;
}
getLength(10); // ❌ Error en compile-time
TypeScript no reemplaza tests, pero elimina una clase enorme de bugs antes de ejecutar.
TS vs JS (mental model)
| JavaScript | TypeScript |
|---|---|
| Dinámico | Estático (en desarrollo) |
| Errores en runtime | Muchos errores en compile-time |
| Flexible | Flexible + contratos |
Archivos .js | Archivos .ts / .tsx |
Al final, TypeScript compila a JavaScript. El navegador/Node sigue ejecutando JS.
//2. Setup mínimo (sin dolor)
Opción A — playground
Entra a typescriptlang.org/play y prueba snippets.
Opción B — proyecto Node rápido
npm init -y
npm i -D typescript tsx @types/node
npx tsc --init
tsconfig.json recomendado (base moderna):
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}
Activa strict: true desde el día 1. Aprender TS “relajado” enseña malos hábitos.
Ejecutar:
npx tsx src/index.ts
//3. Tipos básicos y anotaciones
const level: number = 3;
const username: string = "cami";
const active: boolean = true;
const tags: string[] = ["js", "ts"];
const scores: Array<number> = [10, 9, 8];
let maybe: string | null = null;
maybe = "hola";
Inferencia (deja que TS trabaje)
const puerto = 3001; // number inferido
const title = "Blog"; // string inferido
function sum(a: number, b: number) {
return a + b; // number inferido
}
Anota cuando:
- exportas APIs públicas
- el compilador no puede inferir bien
- quieres documentar intención (contratos)
//4. Objetos: type vs interface
type UserId = string;
interface User {
id: UserId;
name: string;
email: string;
role?: "USER" | "ADMIN"; // opcional
}
const user: User = {
id: "u_1",
name: "Ana",
email: "ana@programemos.dev",
};
Regla práctica del equipo:
interface→ objetos/extensibles (modelos de dominio)type→ uniones, tuples, utilidades, aliases
type Resultado = { ok: true; data: string } | { ok: false; error: string };
type Point = [number, number];
Readonly y as const
interface Config {
readonly apiUrl: string;
}
const roles = ["USER", "ADMIN"] as const;
type Role = (typeof roles)[number]; // "USER" | "ADMIN"
//5. Uniones, narrowing y type guards
function printId(id: string | number) {
if (typeof id === "string") {
console.log(id.toUpperCase());
} else {
console.log(id.toFixed(0));
}
}
Con objetos discriminados (patrón oro en APIs):
type ApiResponse<T> =
| { status: "success"; data: T }
| { status: "error"; message: string };
function handle(res: ApiResponse<User>) {
if (res.status === "success") {
console.log(res.data.email);
} else {
console.error(res.message);
}
}
Type guard custom:
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"email" in value
);
}
//6. Funciones tipadas como adulto
function crearSlug(title: string, max = 80): string {
return title
.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "")
.slice(0, max);
}
type Matcher = (value: string) => boolean;
const startsWithA: Matcher = (value) => value.startsWith("a");
Overloads (cuando hace falta)
function parse(input: string): object;
function parse(input: string, asArray: true): object[];
function parse(input: string, asArray?: boolean) {
const data = JSON.parse(input);
return asArray ? (Array.isArray(data) ? data : [data]) : data;
}
//7. Genéricos: reutiliza sin perder tipos
function first<T>(items: T[]): T | undefined {
return items[0];
}
first([1, 2, 3]); // number | undefined
first(["a", "b"]); // string | undefined
En wrappers de fetch:
async function getJson<T>(url: string): Promise<T> {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<T>;
}
interface Post {
id: string;
title: string;
slug: string;
}
const posts = await getJson<Post[]>("/blog/posts");
Constraints:
function getId<T extends { id: string }>(entity: T): string {
return entity.id;
}
//8. Utility types que sí se usan en el trabajo
interface BlogPost {
id: string;
title: string;
slug: string;
content: string;
publishedAt: Date | null;
}
type CreatePostInput = Omit<BlogPost, "id" | "publishedAt">;
type PostPreview = Pick<BlogPost, "title" | "slug">;
type PartialPost = Partial<BlogPost>;
type ReadonlyPost = Readonly<BlogPost>;
type Status = "DRAFT" | "PUBLISHED";
type StatusMap = Record<Status, number>;
Awaited, ReturnType, Parameters también aparecen mucho en librerías y SDKs.
//9. null, undefined y optional chaining
Con strictNullChecks (incluido en strict), esto falla:
function getName(user: User | null) {
return user.name; // ❌
}
Corrige con narrowing:
function getName(user: User | null) {
if (!user) return "Invitado";
return user.name;
}
O con encadenamiento:
user?.profile?.username ?? "anon";
Evita ! (non-null assertion) salvo casos muy justificados:
const el = document.getElementById("root")!; // peligroso si puede ser null
//10. Tipar datos del mundo real (APIs, forms, JSON)
El borde del sistema es donde vive el caos. Tipar “por fe” no basta.
Estrategia recomendada
- Define el tipo esperado
- Valida en runtime (Zod / class-validator)
- Infiere el tipo desde el schema
import { z } from "zod";
const CreatePostSchema = z.object({
title: z.string().min(3),
excerpt: z.string().min(10),
content: z.string().min(20),
featured: z.boolean().default(false),
});
type CreatePostDto = z.infer<typeof CreatePostSchema>;
function createPost(input: unknown) {
const dto: CreatePostDto = CreatePostSchema.parse(input);
return dto;
}
En NestJS ya usas class-validator + DTOs: es el mismo principio.
//11. TypeScript en frontend (React / Next)
type ButtonProps = {
label: string;
onClick: () => void;
variant?: "primary" | "ghost";
disabled?: boolean;
};
export function Button({ label, onClick, variant = "primary", disabled }: ButtonProps) {
return (
<button data-variant={variant} disabled={disabled} onClick={onClick}>
{label}
</button>
);
}
Props children:
type CardProps = {
title: string;
children: React.ReactNode;
};
Para estado:
const [posts, setPosts] = useState<Post[]>([]);
//12. TypeScript en backend (NestJS mental model)
En Nest/Prisma sueles combinar:
- DTOs (entrada HTTP validada)
- tipos Prisma (
Prisma.BlogPostGetPayload) - tipos de respuesta para el frontend
type PostListItem = {
id: string;
title: string;
slug: string;
excerpt: string;
publishedAt: string | null;
};
No expongas el modelo de DB crudo si el contrato público es distinto (fechas ISO, status en minúsculas, seo anidado, etc.).
//13. Errores típicos (y cómo salir)
1) Abusar de any
// ❌
function load(data: any) {
return data.items.map((x: any) => x.name);
}
// ✅
type Payload = { items: Array<{ name: string }> };
function load(data: Payload) {
return data.items.map((x) => x.name);
}
Si no sabes el tipo: usa unknown + narrowing.
2) Pelearte con el compilador en vez de modelar mejor
Si TS “molesta”, casi siempre el modelo de datos es ambiguo. Mejora el tipo, no lo apagues.
3) Enums numéricos confusos
Prefiere unions de strings:
type ContentStatus = "DRAFT" | "PUBLISHED" | "ARCHIVED";
4) Tipar de más demasiado pronto
Empieza por funciones públicas, DTOs y bordes de red. No tipifiques cada variable local trivial.
//14. Mini proyecto: tipar un cliente de Blog
Objetivo: crear un módulo blog-api.ts tipado contra los endpoints de Programemos.
export type BlogSeo = {
title: string;
description: string;
canonical: string;
robots: string;
};
export type BlogPostDetail = {
id: string;
title: string;
slug: string;
excerpt: string;
content: string;
seo: BlogSeo;
};
export async function getPostBySlug(slug: string): Promise<BlogPostDetail> {
const res = await fetch(`https://api.example.com/blog/posts/${slug}`);
if (!res.ok) throw new Error("Post no encontrado");
return res.json();
}
Extensiones del ejercicio:
- Tipar listado paginado (
items,total,page) - Tipar filtros (
category,tag,featured) - Agregar Zod al response si no confías en el backend (defensa en profundidad)
//15. Plan de 14 días para afianzar TypeScript
| Día | Tema | Práctica |
|---|---|---|
| 1–2 | Tipos básicos + strict | Migrar 1 archivo JS → TS |
| 3–4 | Interfaces / unions | Modelar User + ApiResponse |
| 5–6 | Genéricos | getJson<T> + helpers de array |
| 7–8 | Utility types | DTOs Create/Update |
| 9–10 | Narrowing + unknown | Parsear JSON externo |
| 11–12 | React o Nest tipado | 1 componente o 1 endpoint |
| 13–14 | Mini cliente Blog | Repo limpio en GitHub |
//16. Checklist “ya sé TypeScript de verdad”
- Trabajo con
strict: truesin miedo - Sé cuándo usar
typevsinterface - Domino unions + narrowing
- Escribo genéricos simples sin copiar Stack Overflow
- Uso
Omit,Pick,Partial,Record - Evito
any; usounknowncuando corresponde - Tipo DTOs de entrada/salida en mis APIs
- Puedo migrar un módulo JS existente a TS
//17. Migrar un proyecto JS a TS sin morir en el intento
Estrategia que funciona en equipos reales:
- Activa TypeScript en modo permisivo solo temporalmente (
allowJs) - Renombra primero los módulos de borde (API, utils, shared)
- Enciende
strictpor carpeta/proyecto cuando compile limpio - Sustituye
anypor tipos reales en PRs pequeños - Tipa tests al final (o en paralelo si usas Vitest/Jest)
{
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"strict": true,
"noImplicitAny": true
}
}
No intentes tipar 20.000 líneas en un fin de semana. Migración = pagar deuda en cuotas.
//18. Patrones avanzados que verás en código senior
Branded types (IDs que no se mezclan)
type UserId = string & { readonly __brand: "UserId" };
type PostId = string & { readonly __brand: "PostId" };
function getPost(id: PostId) {}
// getPost(userId) → error si los brands difieren
Exhaustive checks con never
type Shape = { kind: "circle"; r: number } | { kind: "square"; size: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.r ** 2;
case "square":
return shape.size ** 2;
default: {
const _exhaustive: never = shape;
return _exhaustive;
}
}
}
Satisfies (TS 4.9+)
const routes = {
home: "/",
blog: "/blog",
} satisfies Record<string, `/${string}`>;
//19. 20 ejercicios de TypeScript (con intención)
- Tipa una función
paginate<T>(items, page, limit). - Crea
ApiResult<T>success/error y un handler exhaustive. - Define
DeepPartial<T>(recursivo simple). - Tipa un formulario de registro (union de pasos).
- Escribe un type guard
isNonEmptyString. - Modela un carrito con
Readonlye inmutabilidad. - Usa
Omitpara Create/Update de un Post. - Tipa
localStorageget/set con genéricos. - Crea
KeyOfType<T, V>(claves cuyo valor es V). - Tipa un event bus simple (
on/emit). - Migra un utils.js a utils.ts sin
any. - Tipa respuesta paginada del blog.
- Usa Zod +
z.inferen un DTO. - Tipa props de un Modal accesible.
- Crea
assertNever(x: never). - Tipa errores custom (
class AppError). - Haz
fetchtipado con timeout y abort. - Modela permisos:
can(user, action, resource). - Tipa un mapper DB → DTO público.
- Escribe tests type-level con
expectTypeOf(Vitest).
//20. Cómo leer errores del compilador (sin frustrarte)
El mensaje de TS suele ser verboso, pero la clave está arriba:
- Lee el tipo esperado vs el tipo recibido
- Identifica si falta narrowing (
null/undefined) - Pregunta: ¿mi modelo refleja la realidad del dato?
- Si el error es enorme, simplifica el ejemplo en el playground
Ejemplo mental:
Type 'string | undefined' is not assignable to type 'string'
Solución típica: default (??), early return, o hacer el campo opcional en el destino.
//21. Cuándo NO usar TypeScript
- Scripts de 20 líneas desechables
- Prototipos ultra rápidos que vas a tirar hoy
- Equipos que aún no entienden JS básico (primero JS)
Para producto serio, aprendizaje estructurado y backends/frontends medianos+: sí, TS.
//Conclusión
TypeScript no es burocracia: es diseño de contratos. Cuando tipas bien, el editor se vuelve un compañero de pair programming y tus refactors dejan de romper todo en silencio.
Siguiente paso recomendado en Programemos:
- Termina la ruta de TypeScript desde cero
- Tipa un proyecto JS que ya tengas
- Completa 10 ejercicios de esta guía
- Publica un artículo o ejercicio resolviendo un bug real que TS te habría prevenido
Si JavaScript te enseñó a construir, TypeScript te enseña a construir con intención.
FAQ
¿Necesito dominar JavaScript antes de TypeScript?
Sí, al menos a nivel intermedio: funciones, arrays/objetos, módulos y async/await. TypeScript se apoya en esos conceptos.
¿TypeScript se ejecuta en el navegador?
No directamente. TypeScript se compila a JavaScript; el runtime sigue siendo JS en navegador o Node.
¿Debo usar any si estoy empezando?
No. Prefiere unknown + narrowing. any apaga el tipado y elimina la ventaja de TypeScript.
¿type o interface?
Usa interface para objetos de dominio extensibles y type para uniones, tuples y aliases. Ambos son válidos; la consistencia del equipo importa más.
¿TypeScript reemplaza las pruebas?
No. Evita muchos bugs de contratos y formas de datos, pero la lógica de negocio sigue necesitando tests.