Tipos literales y su utilidad
Los tipos literales en TypeScript representan un subconjunto específico de valores dentro de un tipo más amplio. A diferencia de los tipos básicos como string
o number
que pueden contener cualquier cadena o número, los tipos literales te permiten restringir los valores posibles a opciones concretas y predefinidas.
// Tipo string normal (puede ser cualquier cadena)
let direccion: string = "Norte";
direccion = "Sur"; // Válido
direccion = "Cualquier texto"; // Válido
// Tipo literal de cadena (solo permite un valor específico)
let direccionRestringida: "Norte" = "Norte";
direccionRestringida = "Sur"; // Error: El tipo '"Sur"' no se puede asignar al tipo '"Norte"'
Tipos literales de string y number
Los tipos literales pueden ser de varios tipos primitivos:
// Tipo literal de cadena
let respuesta: "sí" | "no" | "tal vez" = "sí";
// Tipo literal numérico
let nivel: 1 | 2 | 3 = 1;
nivel = 2; // Válido
nivel = 4; // Error: El tipo '4' no se puede asignar al tipo '1 | 2 | 3'
Combinación con uniones de tipos
La verdadera potencia de los tipos literales aparece cuando los combinamos con uniones, permitiendo crear un conjunto limitado de opciones válidas:
// Tipo que solo acepta cuatro valores específicos
type Direccion = "Norte" | "Sur" | "Este" | "Oeste";
let miDireccion: Direccion = "Norte"; // Válido
miDireccion = "Sur"; // Válido
miDireccion = "Arriba"; // Error: El tipo '"Arriba"' no se puede asignar al tipo 'Direccion'
// Combinando literales de diferentes tipos
type Respuesta = "sí" | "no" | 1 | 0 | boolean;
Aplicaciones prácticas
Los tipos literales son especialmente útiles en APIs y funciones donde queremos garantizar que solo se pasen valores específicos:
// Función que solo acepta métodos HTTP específicos
function realizarPeticion(url: string, metodo: "GET" | "POST" | "PUT" | "DELETE") {
console.log(`Realizando petición ${metodo} a ${url}`);
// Implementación...
}
realizarPeticion("https://api.ejemplo.com/datos", "GET"); // Válido
realizarPeticion("https://api.ejemplo.com/datos", "PATCH"); // Error: El argumento de tipo '"PATCH"' no es asignable...
También son excelentes para configuraciones con opciones limitadas:
interface OpcionesBoton {
color: "primario" | "secundario" | "peligro" | "éxito";
tamaño: "pequeño" | "mediano" | "grande";
tipo: "submit" | "reset" | "button";
}
const botonGuardar: OpcionesBoton = {
color: "primario",
tamaño: "mediano",
tipo: "submit"
};
Inferencia con tipos literales
TypeScript puede inferir tipos literales en ciertas situaciones, especialmente cuando usamos const
:
// TypeScript infiere el tipo 'Norte' (no string)
const direccionFija = "Norte";
// TypeScript infiere el tipo string (no 'Norte')
let direccionVariable = "Norte";
// Forzar inferencia de tipos literales con as const
let direccionComoLiteral = "Norte" as const;
// También funciona con objetos y arrays
const configuracion = {
tema: "oscuro",
animaciones: true,
nivel: 3
} as const; // Todos los campos se convierten en tipos literales
Tipos condicionales con extends
Concepto básico de tipos condicionales
Los tipos condicionales en TypeScript permiten crear tipos que dependen de una condición, similar a cómo funcionan las expresiones condicionales en JavaScript. Estos tipos actúan como un operador ternario a nivel de tipos.
La sintaxis básica de un tipo condicional es:
T extends U ? X : Y
Donde:
T extends U
es la condición que se evalúaX
es el tipo que se selecciona si la condición es verdaderaY
es el tipo que se selecciona si la condición es falsa
Ejemplos básicos de tipos condicionales
// Tipo que verifica si T es string
type EsString<T> = T extends string ? true : false;
// Evaluación de tipos
type Resultado1 = EsString<"hola">; // true
type Resultado2 = EsString<42>; // false
type Resultado3 = EsString<string>; // true
type Resultado4 = EsString<string | number>; // boolean (true | false)
La palabra clave extends en tipos condicionales
En tipos condicionales, extends
establece una relación de "es asignable a" o "es subtipo de". Cuando escribimos T extends U
, estamos preguntando si T tiene al menos todas las propiedades y comportamientos de U.
// Verificar si un tipo contiene cierta propiedad
type TieneNombre<T> = T extends { nombre: string } ? true : false;
type ConNombre = { nombre: string; edad: number };
type SinNombre = { id: number; activo: boolean };
type Prueba1 = TieneNombre<ConNombre>; // true
type Prueba2 = TieneNombre<SinNombre>; // false
Tipos condicionales con genéricos
Los tipos condicionales son especialmente útiles cuando se combinan con genéricos para crear tipos adaptables:
// Obtener el tipo de elemento de un array
type ElementoDeArray<T> = T extends Array<infer E> ? E : never;
type NumeroDeArray = ElementoDeArray<number[]>; // number
type StringDeArray = ElementoDeArray<string[]>; // string
type NoEsArray = ElementoDeArray<boolean>; // never
// Extraer el tipo de retorno de una función
type TipoRetorno<T> = T extends (...args: any[]) => infer R ? R : never;
function obtenerUsuario() {
return { id: 1, nombre: "Ana" };
}
type Usuario = TipoRetorno<typeof obtenerUsuario>; // { id: number, nombre: string }
Tipos condicionales anidados
Podemos crear lógica más compleja anidando tipos condicionales:
type TipoDeValor<T> =
T extends string ? "es-texto" :
T extends number ? "es-numero" :
T extends boolean ? "es-booleano" :
T extends undefined ? "es-indefinido" :
T extends null ? "es-nulo" :
"es-otro-tipo";
type R1 = TipoDeValor<"hola">; // "es-texto"
type R2 = TipoDeValor<42>; // "es-numero"
type R3 = TipoDeValor<true>; // "es-booleano"
Aplicaciones prácticas de tipos condicionales
Los tipos condicionales permiten crear abstracciones de tipos potentes:
// Tipo que convierte un tipo en opcional o requerido según un parámetro booleano
type HacerOpcional<T, K extends keyof T, Optional extends boolean> =
Optional extends true
? Omit<T, K> & Partial<Pick<T, K>>
: T;
interface Producto {
id: number;
nombre: string;
precio: number;
}
// El campo 'precio' se vuelve opcional
type ProductoConPrecioOpcional = HacerOpcional<Producto, 'precio', true>;
// Equivale a: { id: number; nombre: string; precio?: number; }
Discriminated unions y type narrowing
Guarda tu progreso
Inicia sesión para no perder tu progreso y accede a miles de tutoriales, ejercicios prácticos y nuestro asistente de IA.
Más de 25.000 desarrolladores ya confían en CertiDevs
Concepto de uniones discriminadas
Las discriminated unions (uniones discriminadas) son un patrón de diseño de tipos en TypeScript que combina uniones de tipos con propiedades discriminadoras. Estas uniones tienen una propiedad común (el discriminador) cuyo valor literal permite identificar qué variante de la unión está siendo utilizada.
// Definición básica de una unión discriminada
type Circle = {
kind: "circle"; // Propiedad discriminadora
radius: number; // Propiedades específicas
};
type Rectangle = {
kind: "rectangle"; // Mismo nombre de propiedad, valor diferente
width: number; // Propiedades específicas
height: number;
};
// La unión discriminada
type Shape = Circle | Rectangle;
Trabajando con uniones discriminadas
La principal ventaja de las uniones discriminadas es que TypeScript puede inferir automáticamente el tipo específico después de comprobar el valor del discriminador:
function calculateArea(shape: Shape): number {
// Comprobando el discriminador
if (shape.kind === "circle") {
// TypeScript sabe que shape es Circle aquí
return Math.PI * shape.radius ** 2;
} else {
// TypeScript sabe que shape es Rectangle aquí
return shape.width * shape.height;
}
}
const myCircle: Shape = { kind: "circle", radius: 5 };
console.log(calculateArea(myCircle)); // 78.54...
Type narrowing (estrechamiento de tipos)
El estrechamiento de tipos es una técnica que permite refinar el tipo de una variable dentro de un bloque de código. Esto es esencial para trabajar de forma segura con tipos de unión.
Estrechamiento con typeof
function printValue(value: string | number) {
if (typeof value === "string") {
// En este bloque, TypeScript sabe que value es un string
console.log(value.toUpperCase());
} else {
// En este bloque, TypeScript sabe que value es un number
console.log(value.toFixed(2));
}
}
Estrechamiento con instanceof
class Customer {
constructor(public name: string, public email: string) {}
sendEmail() {
console.log(`Enviando email a ${this.email}`);
}
}
class Employee {
constructor(public name: string, public department: string) {}
assignTask() {
console.log(`Asignando tarea a ${this.name} del departamento ${this.department}`);
}
}
function processUser(user: Customer | Employee) {
console.log(`Procesando usuario: ${user.name}`);
if (user instanceof Customer) {
// TypeScript sabe que user es Customer aquí
user.sendEmail();
} else {
// TypeScript sabe que user es Employee aquí
user.assignTask();
}
}
Estrechamiento con operador in
interface Article {
title: string;
content: string;
publishDate: Date;
}
interface Video {
title: string;
duration: number;
resolution: string;
}
function displayMedia(media: Article | Video) {
console.log(`Título: ${media.title}`);
if ("content" in media) {
// TypeScript sabe que media es Article
console.log(`Contenido: ${media.content.substring(0, 100)}...`);
console.log(`Publicado: ${media.publishDate.toLocaleDateString()}`);
} else {
// TypeScript sabe que media es Video
console.log(`Duración: ${media.duration} segundos`);
console.log(`Resolución: ${media.resolution}`);
}
}
Uso de discriminadores con switch
Las uniones discriminadas funcionan especialmente bien con declaraciones switch:
// Ampliamos nuestra unión con un nuevo tipo
type Triangle = {
kind: "triangle";
base: number;
height: number;
};
// Unión discriminada actualizada
type Shape = Circle | Rectangle | Triangle;
function calculateArea(shape: Shape): number {
// Usando switch con el discriminador
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
default:
// Comprobación de exhaustividad
const _exhaustiveCheck: never = shape;
throw new Error(`Tipo no soportado: ${_exhaustiveCheck}`);
}
}
Predicados de tipo (type predicates)
Los predicados de tipo son funciones que devuelven un valor booleano y tienen una firma especial que le indica a TypeScript cómo debe estrechar el tipo:
// Definimos un predicado de tipo con la sintaxis "paramName is Type"
function isArticle(media: Article | Video): media is Article {
return (media as Article).content !== undefined;
}
function displayMediaWithPredicate(media: Article | Video) {
if (isArticle(media)) {
// TypeScript sabe que media es Article aquí
console.log(`Extracto: ${media.content.substring(0, 50)}...`);
} else {
// TypeScript sabe que media es Video aquí
console.log(`Ver video (${media.duration}s): ${media.title}`);
}
}
Aplicaciones prácticas de uniones discriminadas
Las uniones discriminadas son especialmente útiles en varios escenarios:
Gestión de estados en aplicaciones
type LoadingState = {
status: "loading";
};
type SuccessState<T> = {
status: "success";
data: T;
};
type ErrorState = {
status: "error";
error: string;
};
type State<T> = LoadingState | SuccessState<T> | ErrorState;
// Renderiza diferente UI según el estado
function renderData(state: State<string[]>) {
switch (state.status) {
case "loading":
return "Cargando datos...";
case "success":
return `Datos: ${state.data.join(", ")}`;
case "error":
return `Error: ${state.error}`;
}
}
Modelado de acciones en sistemas de reducción de estado
type AddTodoAction = {
type: "ADD_TODO";
text: string;
};
type ToggleTodoAction = {
type: "TOGGLE_TODO";
id: number;
};
type DeleteTodoAction = {
type: "DELETE_TODO";
id: number;
};
type TodoAction = AddTodoAction | ToggleTodoAction | DeleteTodoAction;
interface Todo {
id: number;
text: string;
completed: boolean;
}
function todoReducer(state: Todo[], action: TodoAction): Todo[] {
switch (action.type) {
case "ADD_TODO":
return [...state, {
id: Date.now(),
text: action.text,
completed: false
}];
case "TOGGLE_TODO":
return state.map(todo =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
);
case "DELETE_TODO":
return state.filter(todo => todo.id !== action.id);
}
}
Conclusión
Los tipos literales y condicionales en TypeScript proporcionan herramientas poderosas para crear sistemas de tipos precisos y flexibles:
- Tipos literales permiten restringir valores a opciones específicas, creando APIs más claras y seguras.
- Tipos condicionales con
extends
habilitan la creación de tipos dinámicos que responden a condiciones. - Uniones discriminadas y el estrechamiento de tipos facilitan el trabajo con estructuras de datos complejas de manera segura.
Estas características, combinadas con lo que ya hemos aprendido sobre uniones e intersecciones, nos permiten expresar relaciones complejas entre tipos y garantizar la corrección de nuestro código en tiempo de compilación, reduciendo errores y mejorando la mantenibilidad.
Aprendizajes de esta lección
- Comprender qué son y cómo usar los tipos literales para restringir valores específicos.
- Aprender a crear y aplicar tipos condicionales para definir tipos dinámicos basados en condiciones.
- Entender el uso de la palabra clave extends para establecer relaciones y restricciones entre tipos.
- Conocer cómo TypeScript realiza inferencia condicional para refinar tipos en tiempo de compilación.
- Aplicar estos conceptos para escribir código más seguro, robusto y expresivo en TypeScript.
Completa TypeScript y certifícate
Únete a nuestra plataforma y accede a miles de tutoriales, ejercicios prácticos, proyectos reales y nuestro asistente de IA personalizado para acelerar tu aprendizaje.
Asistente IA
Resuelve dudas al instante
Ejercicios
Practica con proyectos reales
Certificados
Valida tus conocimientos
Más de 25.000 desarrolladores ya se han certificado con CertiDevs