Skip to main content

Ejercicios de Typescript

Este archivo contiene 30 ejercicios pensados para practicar TypeScript. Cada ejercicio tiene:

  • Código en JavaScript (punto de partida).
  • Una pista que indica la característica de TypeScript a utilizar y para qué sirve.
  • La solución oculta usando <details> para que los estudiantes puedan intentar primero.

No están separados por niveles; la dificultad aumenta de forma gradual.


Ejercicio 1: Suma simple (tipos básicos)​

Ejercicio 1: Suma simple (tipos básicos)
// Convierte a TypeScript
function sumar(a, b) {
return a + b;
}

Pista: Usa tipos number en los parámetros y en el tipo de retorno. number sirve para representar valores numéricos y ayuda a evitar pasar strings por error.

Ver solución
Ejercicio 1: Suma simple (tipos básicos) (2)
function sumar(a: number, b: number): number {
return a + b;
}

Ejercicio 2: Parámetro opcional y por defecto​

Parámetro opcional y por defecto
// Convierte a TypeScript
function saludar(nombre, saludo) {
saludo = saludo || "Hola";
return `${saludo}, ${nombre}`;
}

Pista: Usa ? para parámetros opcionales y valores por defecto. param?: T indica que puede omitirse y = valor da un valor por defecto.

Ver solución
Parámetro opcional y por defecto (2)
function saludar(nombre: string, saludo: string = "Hola"): string {
return `${saludo}, ${nombre}`;
}

Ejercicio 3: Definir un objeto con interfaz​

Definir un objeto con interfaz
// Convierte a TypeScript
const crearUsuario = (u) => {
return `Usuario ${u.nombre} creado con id ${u.id}`;
};

Pista: Usa interface para declarar la forma del objeto Usuario. interface sirve para definir contratos de forma clara entre partes del código.

Ver solución
Definir un objeto con interfaz (2)
interface Usuario {
id: number;
nombre: string;
email?: string; // opcional
}

const crearUsuario = (u: Usuario): string => {
return `Usuario ${u.nombre} creado con id ${u.id}`;
};

Ejercicio 4: Readonly y tipos literales​

Ejercicio 4: Readonly y tipos literales
// Convierte a TypeScript
const config = {
mode: "production",
version: "1.0.0"
};

Pista: Usa readonly para propiedades inmutables y un union type literal (por ejemplo "dev" | "prod"). readonly evita reasignaciones accidentales.

Ver solución
Ejercicio 4: Readonly y tipos literales (2)
type Mode = "development" | "production" | "test";

interface Config {
readonly mode: Mode;
readonly version: string;
}

const config: Config = {
mode: "production",
version: "1.0.0"
};

// config.version = '1.0.1' // Error: no se puede asignar a 'version' porque es readonly

Ejercicio 5: Tuplas​

Ejercicio 5: Tuplas
// Convierte a TypeScript
function crearPar(nombre, edad) {
return [nombre, edad];
}

Pista: Usa una tupla [string, number] para representar (nombre, edad). Las tuplas permiten tipos fijos en posiciones concretas.

Ver solución
Ejercicio 5: Tuplas (2)
function crearPar(nombre: string, edad: number): [string, number] {
return [nombre, edad];
}

const p = crearPar("Ana", 30);

Ejercicio 6: Enums para estados​

Ejercicio 6: Enums para estados
// Convierte a TypeScript
const estado = "Activo"; // puede ser 'Activo' o 'Inactivo'

Pista: Usa enum para representar un conjunto de valores nombrados. enum ayuda a mantener valores constantes legibles y seguros.

Ver solución
Ejercicio 6: Enums para estados (2)
enum Estado {
Activo = "Activo",
Inactivo = "Inactivo"
}

const estado: Estado = Estado.Activo;

Ejercicio 7: Genéricos: identidad​

Ejercicio 7: Genéricos: identidad
// Convierte a TypeScript
function identidad(x) {
return x;
}

Pista: Usa generics (<T>) para que la función preserve y acepte cualquier tipo. Los genéricos permiten escribir funciones y tipos reutilizables y tipados.

Ver solución
Ejercicio 7: Genéricos: identidad (2)
function identidad<T>(x: T): T {
return x;
}

const a = identidad<number>(123);
const b = identidad("hola");

Ejercicio 8: Genéricos con restricción (length)​

Genéricos con restricción (length)
// Convierte a TypeScript
function cuentaLongitud(x) {
return x.length;
}

Pista: Usa T extends { length: number } para restringir a tipos que tengan length. Esto garantiza que x.length existe.

Ver solución
Genéricos con restricción (length) (2)
function cuentaLongitud<T extends { length: number }>(x: T): number {
return x.length;
}

console.log(cuentaLongitud('hola')); // 4
console.log(cuentaLongitud([1,2,3])); // 3

Ejercicio 9: Unión de tipos y narrowing​

Ejercicio 9: Unión de tipos y narrowing
// Convierte a TypeScript
function formatear(x) {
if (typeof x === 'number') {
return x.toFixed(2);
}
return x.trim();
}

Pista: Usa string | number y typeof para hacer narrowing. typeof permite distinguir tipos primitivos en tiempo de ejecución.

Ver solución
Ejercicio 9: Unión de tipos y narrowing (2)
function formatear(x: string | number): string {
if (typeof x === 'number') {
return x.toFixed(2);
}
return x.trim();
}

Ejercicio 10: Type guard con in​

Ejercicio 10: Type guard
// Convierte a TypeScript
function descripcion(animal) {
if (animal.nombre) {
return `Animal: ${animal.nombre}`;
}
return 'Anónimo';
}

Pista: Usa in para comprobar presencia de una propiedad (p.ej. if ('nombre' in animal)). Esto sirve como type guard para objetos con distintas formas.

Ver solución
Ejercicio 10: Type guard (2)
type Gato = { nombre: string; ronronea: boolean };
type Pez = { tipo: string; nada: boolean };

function descripcion(animal: Gato | Pez): string {
if ('nombre' in animal) {
return `Animal: ${animal.nombre}`;
}
return 'Anónimo';
}

Ejercicio 11: Narrowing con instanceof​

Ejercicio 11: Narrowing con instanceof
// Convierte a TypeScript
class A { constructor() {} }
class B { constructor() {} }

function esA(x) {
if (x instanceof A) return true;
return false;
}

Pista: Usa clases y instanceof para distinguir instancias en tiempo de ejecución. instanceof es útil con clases y permite inferir el tipo dentro del bloque.

Ver solución
Ejercicio 11: Narrowing con instanceof (2)
class A { a = 1 }
class B { b = 2 }

function esA(x: A | B): boolean {
if (x instanceof A) return true;
return false;
}

const a = new A();
console.log(esA(a));

Ejercicio 12: Uniones discriminadas (formas geométricas)​

Uniones discriminadas
// Convierte a TypeScript
function area(shape) {
if (shape.kind === 'circle') {
return Math.PI * shape.radius * shape.radius;
}
return shape.size * shape.size;
}

Pista: Usa type con una propiedad literal discriminante kind. Las uniones discriminadas facilitan el narrowing y la seguridad de tipos.

Ver solución
Uniones discriminadas (2)
type Circulo = { kind: 'circle'; radius: number };
type Cuadrado = { kind: 'square'; size: number };
type Shape = Circulo | Cuadrado;

function area(shape: Shape): number {
if (shape.kind === 'circle') {
return Math.PI * shape.radius * shape.radius;
}
return shape.size * shape.size;
}

Ejercicio 13: keyof y pluck​

Ejercicio 13: keyof y pluck
// Convierte a TypeScript
function pluck(obj, keys) {
return keys.map(k => obj[k]);
}

Pista: Usa keyof y genéricos K extends keyof T para asegurar que las claves existen en obj. keyof obtiene las claves de un tipo.

Ver solución
Ejercicio 13: keyof y pluck (2)
function pluck<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
return keys.map(k => obj[k]);
}

const persona = { nombre: 'Ana', edad: 28 };
const resultados = pluck(persona, ['nombre']); // tipo: string[]

Ejercicio 14: Utility type: Pick​

Ejercicio 14: Utility type: Pick
// Convierte a TypeScript
const user = { id: 1, nombre: 'Ana', password: '123' };
// Queremos un objeto público sin password

Pista: Usa Pick<T, K> para construir un nuevo tipo con solo algunas propiedades. Pick sirve para exponer solo un subconjunto del tipo original.

Ver solución
Ejercicio 14: Utility type: Pick (2)
interface Usuario {
id: number;
nombre: string;
password: string;
}

type UsuarioPublico = Pick<Usuario, 'id' | 'nombre'>;

const user: UsuarioPublico = { id: 1, nombre: 'Ana' };

Ejercicio 15: Utility type: Omit​

Ejercicio 15: Utility type: Omit
// Convierte a TypeScript
const user = { id: 1, nombre: 'Ana', password: '123' };
// Queremos un usuario sin password

Pista: Usa Omit<T, K> para crear un tipo sin ciertas propiedades. Omit es útil para eliminar campos sensibles como contraseña.

Ver solución
Ejercicio 15: Utility type: Omit (2)
interface Usuario {
id: number;
nombre: string;
password: string;
}

type UsuarioSinPassword = Omit<Usuario, 'password'>;

const safeUser: UsuarioSinPassword = { id: 1, nombre: 'Ana' };

Ejercicio 16: Partial y Required​

Ejercicio 16: Partial y Required
// Convierte a TypeScript
function actualizar(usuario, cambios) {
return { ...usuario, ...cambios };
}

Pista: Usa Partial<T> para permitir objetos con propiedades opcionales en cambios. Partial convierte todas las propiedades en opcionales; Required hace lo contrario.

Ver solución
Ejercicio 16: Partial y Required (2)
interface Usuario {
id: number;
nombre: string;
email?: string;
}

function actualizar(usuario: Usuario, cambios: Partial<Usuario>): Usuario {
return { ...usuario, ...cambios };
}

const u: Usuario = { id: 1, nombre: 'Ana' };
const actualizado = actualizar(u, { email: 'a@b.com' });

Ejercicio 17: ReadonlyArray y readonly​

Ejercicio 17: ReadonlyArray y readonly
// Convierte a TypeScript
const nums = [1,2,3];
nums.push(4); // debería no permitirse si queremos inmutabilidad

Pista: Usa readonly o ReadonlyArray<T> para arrays inmutables. readonly evita métodos mutativos como push o splice en tiempo de compilación.

Ver solución
Ejercicio 17: ReadonlyArray y readonly (2)
const nums: ReadonlyArray<number> = [1, 2, 3];
// nums.push(4); // Error: Property 'push' does not exist on type 'readonly number[]'.

Ejercicio 18: Sobrecarga de funciones​

Ejercicio 18: Sobrecarga de funciones
// Convierte a TypeScript
function combinar(a, b) {
if (typeof a === 'number' && typeof b === 'number') return a + b;
return `${a}${b}`;
}

Pista: Usa overloads (declaraciones de firma) para expresar que la función puede aceptar y devolver distintos tipos. Esto mejora la inferencia para quien la use.

Ver solución
Ejercicio 18: Sobrecarga de funciones (2)
function combinar(a: number, b: number): number;
function combinar(a: string, b: string): string;
function combinar(a: any, b: any): any {
if (typeof a === 'number' && typeof b === 'number') return a + b;
return `${a}${b}`;
}

const n = combinar(1, 2); // number
const s = combinar('a', 'b'); // string

Ejercicio 19: Mapped types: crear Optionalize​

Mapped types: crear Optionalize
// Convierte a TypeScript
// Queremos un tipo igual al original pero con todas las propiedades opcionales

Pista: Crea un mapped type Optionalize<T> con [P in keyof T]?: T[P]. Los mapped types transforman propiedades de un tipo a través de keyof.

Ver solución
Mapped types: crear Optionalize (2)
type Optionalize<T> = { [P in keyof T]?: T[P] };

interface Persona {
id: number;
nombre: string;
}

type PersonaOpcional = Optionalize<Persona>; // { id?: number; nombre?: string }

Ejercicio 20: Tipos condicionales simples​

Tipos condicionales simples
// Convierte a TypeScript
// Queremos un tipo que sea 'yes' si T es string, y 'no' en caso contrario

Pista: Usa T extends U ? X : Y para crear tipos condicionales. Sirven para calcular tipos basados en otros.

Ver solución
Tipos condicionales simples (2)
type IsString<T> = T extends string ? 'yes' : 'no';

type A = IsString<string>; // 'yes'
type B = IsString<number>; // 'no'

Ejercicio 21: unknown vs any​

Ejercicio 21: unknown vs any
// Convierte a TypeScript
function parse(json) {
return JSON.parse(json);
}

Pista: Usa unknown como tipo de retorno de parse y narrowing antes de usarlo. unknown obliga a comprobar el tipo antes de operar, a diferencia de any.

Ver solución
Ejercicio 21: unknown vs any (2)
function parse(json: string): unknown {
return JSON.parse(json);
}

const data = parse('{"x":1}');
if (typeof data === 'object' && data !== null && 'x' in data) {
// ahora TypeScript permite acceso seguro
console.log((data as any).x);
}

Ejercicio 22: never y chequeo exhaustivo​

Ejercicio 22: never y chequeo exhaustivo
// Convierte a TypeScript
function procesar(valor) {
switch(valor) {
case 'a': return 1;
case 'b': return 2;
}
}

Pista: Usa un never en la rama default para forzar exhaustividad (assertNever). never representa valores que nunca ocurren y ayuda a detectar casos no manejados.

Ver solución
Ejercicio 22: never y chequeo exhaustivo (2)
type T = 'a' | 'b';

function assertNever(x: never): never {
throw new Error('Valor inesperado: ' + x);
}

function procesar(valor: T): number {
switch (valor) {
case 'a': return 1;
case 'b': return 2;
default: return assertNever(valor as never);
}
}

Ejercicio 23: Index signatures / Record​

Ejercicio 23: Index signatures / Record
// Convierte a TypeScript
const puntuaciones = {};
puntuaciones['ana'] = 10;
puntuaciones['juan'] = 8;

Pista: Usa Record<string, number> o interface con index signature [k: string]: number para tipar objetos con claves dinámicas.

Ver solución
Ejercicio 23: Index signatures / Record
const puntuaciones: Record<string, number> = {};
puntuaciones['ana'] = 10;
puntuaciones['juan'] = 8;

Ejercicio 24: Módulos: export / import​

Ejercicio 24: Módulos: export / import
// Convierte a TypeScript (dos archivos)
// utils.js
function doble(x) { return x * 2; }
module.exports = { doble };

// index.js
const { doble } = require('./utils');
console.log(doble(3));

Pista: Usa export y import con tipos. export declara lo que se comparte desde un módulo y import lo consume.

Ver solución
utils.ts
// utils.ts
export function doble(x: number): number { return x * 2; }

// index.ts
import { doble } from './utils';
console.log(doble(3));

Ejercicio 25: 'as const' para inferencia literal​

'as const' para inferencia literal
// Convierte a TypeScript
const opciones = { modo: 'auto', retry: 3 };

Pista: Usa as const para fijar valores como literales (readonly). as const convierte propiedades en readonly y sus valores en tipos literales.

Ver solución
'as const' para inferencia literal (2)
const opciones = { modo: 'auto', retry: 3 } as const;
// tipo de opciones.modo es 'auto' (literal), no string

Ejercicio 26: Tuplas con elementos rest​

Ejercicio 26: Tuplas con elementos rest
// Convierte a TypeScript
function makeTuple(first, ...rest) {
return [first, ...rest];
}

Pista: Declara la tupla como [string, ...number[]] por ejemplo. Los rest elements en tuplas permiten mezclar longitud fija con repetición tipada.

Ver solución
Ejercicio 26: Tuplas con elementos rest (2)
function makeTuple(first: string, ...rest: number[]): [string, ...number[]] {
return [first, ...rest];
}

const t = makeTuple('x', 1, 2, 3); // tipo: [string, ...number[]]

Ejercicio 27: Promises y async/await​

Ejercicio 27: Promises y async/await
// Convierte a TypeScript
async function fetchUser() {
const r = await fetch('/user');
return r.json();
}

Pista: Tipa la función con Promise<Usuario> y el método json() con el tipo correcto. Las promesas en TS se declaran como Promise<T>.

Ver solución
Ejercicio 27: Promises y async/await
interface Usuario { id: number; nombre: string; }

async function fetchUser(): Promise<Usuario> {
const r = await fetch('/user');
const data = await r.json() as Usuario;
return data;
}

Ejercicio 28: Type assertion y non-null assertion​

Type assertion y non-null assertion
// Convierte a TypeScript
const el = document.getElementById('app');
el.innerHTML = 'Hola';

Pista: Usa as HTMLElement o ! para indicar a TypeScript que conoces el tipo o que no es null. ! (non-null assertion) asume que el valor no es null/undefined.

Ver solución
Type assertion y non-null assertion (2)
const el = document.getElementById('app') as HTMLElement | null;
if (el) {
el.innerHTML = 'Hola';
}

// o usando non-null assertion (con precaución)
const el2 = document.getElementById('app')!;
el2.innerHTML = 'Hola';