Skip to main content

TypeScript Exercises

This document contains 28 practical exercises designed to master TypeScript. Each exercise includes:

  • Starting JavaScript code.
  • A hint identifying the target TypeScript feature and its purpose.
  • A hidden solution inside <details> allowing students to attempt the problem first.

Exercises progress with gradually increasing difficulty.


Exercise 1: Simple Addition (Basic Types)​

Exercise 1: Simple Addition
// Convert to TypeScript
function sumar(a, b) {
return a + b;
}

Hint: Use number types for both parameters and the return type. number represents numeric values and prevents passing strings by mistake.

View Solution
Exercise 1: Simple Addition 2
function sumar(a: number, b: number): number {
return a + b;
}

Exercise 2: Optional and Default Parameters​

Exercise 2: Optional and Default
// Convert to TypeScript
function saludar(nombre, saludo) {
saludo = saludo || "Hola";
return `${saludo}, ${nombre}`;
}

Hint: Use ? for optional parameters and default assignment values. param?: T indicates optionality, while = defaultValue assigns a fallback value.

View Solution
Exercise 2: Optional and Default 2
function saludar(nombre: string, saludo: string = "Hola"): string {
return `${saludo}, ${nombre}`;
}

Exercise 3: Defining an Object with an Interface​

Exercise 3: Defining an Object
// Convert to TypeScript
const crearUsuario = (u) => {
return `Usuario ${u.nombre} creado con id ${u.id}`;
};

Hint: Use interface to declare the shape of the User object. interface defines clear contracts across parts of your application.

View Solution
Exercise 3: Defining an Object 2
interface Usuario {
id: number;
nombre: string;
email?: string; // opcional
}

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

Exercise 4: Readonly and Literal Types​

Exercise 4: Readonly and Literal Types
// Convert to 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.

View Solution
Exercise 4: Readonly and Literal Types 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

Exercise 5: Tuples​

Exercise 5: Tuples
// Convert to 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.

View Solution
Exercise 5: Tuples 2
function crearPar(nombre: string, edad: number): [string, number] {
return [nombre, edad];
}

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

Exercise 6: Enums for State Management​

Exercise 6: Enums for State Management
// Convert to 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.

View Solution
Exercise 6: Enums for State Management 2
enum Estado {
Activo = "Activo",
Inactivo = "Inactivo"
}

const estado: Estado = Estado.Activo;

Exercise 7: Generics: Identity Function​

Exercise 7: Generics: Identity Function
// Convert to 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.

View Solution
Exercise 7: Generics: Identity 2
function identidad<T>(x: T): T {
return x;
}

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

Exercise 8: Generics with Constraints (length)​

Exercise 8: Generics with Constraints
// Convert to 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.

View Solution
Exercise 8: Generics with Constraints 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

Exercise 9: Union Types and Type Narrowing​

Exercise 9: Union Types and Type
// Convert to 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.

View Solution
Exercise 9: Union Types and Type 2
function formatear(x: string | number): string {
if (typeof x === 'number') {
return x.toFixed(2);
}
return x.trim();
}

Exercise 10: Type Guard with in​

Exercise 10: Type Guard
// Convert to 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.

View Solution
Exercise 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';
}

Exercise 11: Narrowing with instanceof​

Exercise 11: Narrowing with instanceof
// Convert to 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.

View Solution
Exercise 11: Narrowing with 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));

Exercise 12: Discriminated Unions (Geometric Shapes)​

Exercise 12: Discriminated Unions
// Convert to 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.

View Solution
Exercise 12: Discriminated Unions 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;
}

Exercise 13: keyof and Pluck Operator​

Exercise 13: keyof and Pluck Operator
// Convert to 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.

View Solution
Exercise 13: keyof and Pluck Operator 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[]

Exercise 14: Utility Type: Pick​

Exercise 14: Utility Type: Pick
// Convert to 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.

View Solution
Exercise 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' };

Exercise 15: Utility Type: Omit​

Exercise 15: Utility Type: Omit
// Convert to 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.

View Solution
Exercise 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' };

Exercise 16: Partial and Required​

Exercise 16: Partial and Required
// Convert to 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.

View Solution
Exercise 16: Partial and 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' });

Exercise 17: ReadonlyArray and readonly​

Exercise 17: ReadonlyArray and readonly
// Convert to 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.

View Solution
Exercise 17: ReadonlyArray 2
const nums: ReadonlyArray<number> = [1, 2, 3];
// nums.push(4); // Error: Property 'push' does not exist on type 'readonly number[]'.

Exercise 18: Function Overloading​

Exercise 18: Function Overloading
// Convert to 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.

View Solution
Exercise 18: Function Overloading 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

Exercise 19: Mapped Types: Creating Optionalize​

Exercise 19: Mapped Types: Creating
// Convert to 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.

View Solution
Exercise 19: Mapped Types: Creating 2
type Optionalize<T> = { [P in keyof T]?: T[P] };

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

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

Exercise 20: Simple Conditional Types​

Exercise 20: Simple Conditional Types
// Convert to 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.

View Solution
Exercise 20: Simple Conditional Types 2
type IsString<T> = T extends string ? 'yes' : 'no';

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

Exercise 21: unknown vs any​

Exercise 21: unknown vs any
// Convert to 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.

View Solution
Exercise 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);
}

Exercise 22: never and Exhaustive Type Checking​

Exercise 22: never and Exhaustive Type
// Convert to 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.

View Solution
Exercise 22: never and Exhaustive Type 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);
}
}

Exercise 23: Index Signatures / Record​

Exercise 23: Index Signatures / Record
// Convert to 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.

View Solution
Exercise 23: Index Signatures / Record 2
const puntuaciones: Record<string, number> = {};
puntuaciones['ana'] = 10;
puntuaciones['juan'] = 8;

Exercise 24: Modules: export / import​

Exercise 24: Modules: export / import
// Convert to 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.

View Solution
utils.ts
// utils.ts
export function doble(x: number): number { return x * 2; }

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

Exercise 25: 'as const' for Literal Inference​

Exercise 25: 'as const' for Literal
// Convert to 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.

View Solution
Exercise 25: 'as const' for Literal 2
const opciones = { modo: 'auto', retry: 3 } as const;
// tipo de opciones.modo es 'auto' (literal), no string

Exercise 26: Tuples with Rest Elements​

Exercise 26: Tuples with Rest Elements
// Convert to 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.

View Solution
Exercise 26: Tuples with Rest Elements 2
function makeTuple(first: string, ...rest: number[]): [string, ...number[]] {
return [first, ...rest];
}

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

Exercise 27: Promises and async/await​

Exercise 27: Promises and async/await
// Convert to 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>.

View Solution
Exercise 27: Promises and async/await 2
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;
}

Exercise 28: Type Assertion and Non-Null Assertion​

Exercise 28: Type Assertion and Non-Null
// Convert to 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.

View Solution
Exercise 28: Type 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';