Seguridad en Typescript
En la presente entrada exploraremos los conceptos fundamentales de seguridad en TypeScript. Veremos cómo el tipo sistema de TypeScript nos ayuda a escribir código más robusto, entendiendo el espectro de confianza que existe entre la incertidumbre total y la certeza absoluta. Este conocimiento es crucial para construir aplicaciones que manejen datos externos (APIs, bases de datos, inputs de usuario) de manera segura.
El espectro de confianza
| Nivel | Herramienta | Actitud de TS | Cuándo usarlo |
|---|---|---|---|
| 0: Caos | any | "Haz lo que quieras, no estoy mirando". | Nunca (salvo consideraciones excepcionales). |
| 1: Duda | unknown | "Sé que hay algo ahí, pero no te dejaré tocarlo hasta que me pruebes qué es". | Inputs de usuario, APIs, errores en el catch. |
| 2: Contrato | interface / type | "Sé exactamente qué es esto y te avisaré si te falta algo". | Dominio, lógica interna, componentes. |
| 3: Certeza | readonly / const | "Sé qué es y te garantizo que nunca va a cambiar". | Configuraciones, constantes, estados inmutables. |
Los pilares de la seguridad
Para construir código seguro en TypeScript, necesitamos entender tres conceptos fundamentales que nos ayudan a transformar la duda en certeza:
1. unknown: El tipo que lo duda todo
unknown es el tipo más seguro para datos externos. Representa cualquier tipo de valor, pero, a diferencia de any, no te permite hacer nada sin antes validar qué es. Es como un cofre cerrado: sabes que hay algo dentro, pero necesitas una llave (type guard) para abrirlo.
2. Type Guards: Las llaves de validación
Un type guard es una función que comprueba runtime si un valor es de un tipo específico. Usan la palabra clave is para decirle a TypeScript "confía en mí, esto es lo que parece". Son la base para transformar unknown en algo usable.
3. Interfaces y Types: El contrato
Una vez validados los datos, necesitamos definir qué esperamos. Las interfaces y types son contratos que establecen la forma exacta de nuestros datos. Si algo no cumple el contrato, TypeScript nos lo hará saber en tiempo de compilación.
Ejemplo práctico
Imaginemos un programa que sólo nuestra un usuario de una colección expuesta a través de una api. Dicha información la obtenemos por medio de la función getUserById:
1(async function main() {
2 try {
3 const user = await getUserById(1);
4 console.log(user);
5
6 } catch (error) {
7 console.error(error);
8 }
9})();Dentro de getUserById realizamos un fetch a la api de jsonplaceholder y obtenemos el objeto deseado. En el espectro de confianza, estamos en un punto de duda: sé que la api me devuelve algo que yo asumo que es un usuario, pero no tengo control de lo que realmente está llegando.
1async function getUserById(id: number) {
2 const resp = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
3 const resl = await resp.json();
4 return resl;
5}A este punto, es importante recordar que nuestros programas obedecen a los preceptos de arquitectura limpia, y queremos que nuestra aplicación siga lo establecido en el dominio, así como los casos de uso/lógica de negocio establecidos. Dado ese caso, nosotros deberíamos tener definida alguna interfáz User que homologue el comportamiento de estos objetos a lo largo de toda la aplicación.
Por ello, retiraremos la duda de nuestro código implementando nuestra definición en el dominio:
1interface User {
2 username: string;
3 email: string;
4 phone: string;
5};Pero, la definción de User en el dominio en sí misma no nos brinda la certeza que queremos tener respecto a la información consultada. Por ello nos valdremos de un type guard para comprobar que el dato que nos genera duda (representado en este caso con el tipo unknown), es ó, como veremos en el código, se amolda a nuestra definición en el dominio.
1function isUser(data: unknown): data is User {
2 if (
3 typeof data !== 'object' ||
4 data === null
5 ) return false;
6
7 const d = data as Record<string, unknown>;
8
9 return (
10 typeof d.username === 'string' &&
11 typeof d.phone === 'string' &&
12 typeof d.email === 'string'
13 );
14}Y es importante detenernos un momento aquí para revisar algunos conceptos presentes que son interersantes. En primer lugar estamos observando el concepto de type narrowing. Estamos reduciendo o estrechando nuestro rango de incertidumbre cuestionando al objeto aspectos cada ves más específicos de su composición.
para mi consideración, las 3 preguntas fundamentales que nos ayudarían a determianr si el objeto es o no un User (en este caso) serían:
- ¿En realidad es un objeto? podría ser un tipo primitivo o no existir.
- ¿El objeto no es un objeto vacío? recordemos que null es de tipo object.
- ¿El objeto contiene y respeta las propiedades definidas en el dominio? tanto en campo como en tipo de dato.
Ahora que hemos definido nuestro type guard, podemos usarlo para validar el dato que nos llega:
1async function getUserById(id: number) {
2 const resp = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
3 const resl: unknown = await resp.json();
4
5 if (isUser(resl)) {
6 return resl;
7 } else {
8 throw new Error("El formato del usuario recibido es inválido");
9 }
10}Con esto, hemos transformado la duda en certeza. Ahora, si necesitamos obtener múltiples usuarios, también podemos crear un type guard para arrays:
1function isUserArray(data: unknown): data is User[] {
2 return (
3 Array.isArray(data) &&
4 data.every(item => isUser(item))
5 );
6}Zod: Validación declarativa
Aunque los type guards son poderosos, escribir validaciones manualmente puede ser tedioso. Zod es una biblioteca que nos permite definir esquemas de validación de manera declarativa y obtener tipos de TypeScript automáticamente.
1import { z } from 'zod';
2
3const UserSchema = z.object({
4 username: z.string(),
5 email: z.string().email(),
6 phone: z.string(),
7});
8
9// Inferir tipo desde el esquema
10type User = z.infer<typeof UserSchema>;
11
12// Validar datos
13function getUser(id: number): User {
14 const data = fetchUserData(id); // unknown
15
16 return UserSchema.parse(data); // throws if invalid
17}
18
19// Validación segura (no lanza excepción)
20const result = UserSchema.safeParse(data);
21if (result.success) {
22 console.log(result.data.username);
23} else {
24 console.log(result.error.issues);
25}Conclusión
La seguridad en TypeScript no se trata solo de escribir código que compile, sino de construir aplicaciones que resistan la incertidumbre del mundo real. Cada dato que viene de afuera es potencialmente peligroso hasta que no probemos lo contrario.
Siguiendo el espectro de confianza —desde unknown hasta readonly— podemos transformar datos dudosos en certeza verificada. Las herramientas como los type guards, Zod y las discriminated unions son nuestros aliados en esta misión.
Recuerda: nunca confíes en los datos externos. Valida, tipa, y solo después de haber demostrado su forma, úsalos con confianza.