Entendiendo los Módulos en JavaScript y Deno

Typescript es una herramienta que genera opiniones divididas entre los desarrolladores. Por un lado, es reconocido por aportar mayor seguridad y claridad a Javascript. Sin embargo, hay quienes consideran que puede resultar en un código más extenso y menos legible. No obstante, su adopción masiva ha sido increíble, convirtiéndose en un estándar ampliamente aceptado en la industria.

#Un superset de Javascript

A lo largo de los años, ha habido varios intentos por añadir tipado estático a JavaScript, pero TypeScript lo ha logrado de forma efectiva y práctica. Principalmente porque TypeScript es un superset estricto de JavaScript, es decir, todo código JavaScript válido también es código TypeScript válido. De esta manera, no es necesario dominar TypeScript para comenzar a usarlo. Puedes empezar de forma sencilla, utilizando directamente JavaScript.

Por ejemplo, considera el siguiente archivo TypeScript:

main.ts
let var = 39;

Si pasas el cursor sobre var en tu editor, notarás que el tipo ha sido inferido automáticamente como number. Este es un claro ejemplo de cómo TypeScript proporciona tipado estático incluso sin configuraciones o anotaciones explícitas.

#Beneficios del Tipado Estático

Uno de los principales beneficios de TypeScript es la posibilidad de identificar y prevenir errores durante el tiempo de desarrollo, en lugar de encontrarlos durante la ejecución de tu aplicación. Por ejemplo, si intentas asignar un valor con un tipo diferente a una variable ya definida, TypeScript arrojará inmediatamente un error.

main.js
let msg: string = "Hola, mundo";
msg = 39; // Error: Type '39' is not assignable to type 'string'.

Con JavaScript, este tipo de error habría pasado inadvertido hasta el momento de ejecutarlo.

#Inferencia de Tipos

Typescript también puede inferir automáticamente el tipo de una variable basado en su valor inicial. Por ejemplo:

main.ts
const pi = 3.14;

En este caso, TypeScript infiere que pi es del tipo 3.14, numérico, y garantiza que no puedas reasignarle otro valor. Este mecanismo de inferencia ofrece "documentación en vivo" sobre el tipo de datos que maneja cada variable, facilitando la comprensión y el mantenimiento del código.

#Anotación de Tipos

En los casos donde TypeScript no puede inferir el tipo (por ejemplo, variables que no se inicializan inmediatamente), puedes usar anotaciones explícitas:

let id: string;
id = "12345"; // Válido
id = 67890; // Error: Type 'number' is not assignable to type 'string'.

Esto resulta especialmente útil al trabajar con funciones y parámetros, ya que declarar los tipos ayuda a mantener un código claro y robusto.

#Ejecutando Typescript con Deno

En Deno, el tipado de TypeScript en los scripts se elimina al ejecutarlos, ya que el entorno de ejecución compila el código TypeScript a JavaScript estándar. Por ejemplo:

let var: number = 50;
var = "foo";

Si ejecutas este archivo con Deno, no se generará un error de ejecución, incluso si omitimos el tipado explícito. Pero puedes habilitar la validación de tipo al momento de ejecutarlo utilizando el flag --check cuando ejecutes tu script:

deno run --check main.ts

Alternativamente, puede usar el comando deno check para validar los tipos sin ejecutar el coidigo:

deno check archivo.ts

Estos mecanismos te permiten garantizar que tu código siga las reglas de tipado definidas.

#IntelliSense

Uno de los mejores beneficios de TypeScript es su integración con herramientas de desarrollo como VS Code. Al aprovechar características como IntelliSense, puedes obtener información útil sobre los tipos y estructuras de datos directamente en tu editor. Esto es especialmente valioso en proyectos grandes o después de períodos prolongados en los que no hayas trabajado en el código.

Por ejemplo, si defines un tipo explícito en una función o variable, IntelliSense puede ofrecerte sugerencias, completar automáticamente el código o alertarte de posibles errores incluso antes de compilar.

main.ts
function hello(name: string) {
return `Hola, ${name}`;
}

Al utilizar esta función hello en tu IDE, podrás obtener sugerencias detalladas sobre los parámetros que permite, su tipo y el valor de retorno esperado.

#Funciones con Tipado Seguro (Type Safety)

Al trabajar con funciones en TypeScript, es importante garantizar que los parámetros estén correctamente tipados. Consideremos el siguiente ejemplo: queremos crear una función multiply que reciba dos parámetros. Si no especificamos el tipo de los parámetros, TypeScript les asignará por defecto el tipo any.

main.ts
function multiply(a, b) {
return a * b;
}

Al inspeccionar nuestra función en VS Code, el editor inferirá que los parámetros a y b son del tipo any. Esto se debe a que, en ausencia de una anotación explícita o una inferencia clara, TypeScript recurre al tipo any, que permite aceptar cualquier valor. Sin embargo, trabajar con any representa un riesgo, ya que elimina la principal ventaja que tiene TypeScript, que es el tipado estático.

#Evitando el Tipo any

Es importante evitar el uso implícito del tipo any. De hecho, en Deno, el modo estricto de TypeScript está activado por defecto, lo que no permite usar tipos any implícitos. Para corregir nuestro ejemplo, podemos especificar el tipo any en los parámetros a y b.

main.ts
function multiply(a: any, b: any) {
return a * b;
}

Sin embargo, esta solución hará que nuestro IDE muestre una advertencia de que el uso de any no es una práctica recomendada. Aunque los tipos any pueden ser necesarios en algunas situaciones, su uso debe ser limitado y justificado. Si deseas ignorar esta advertencia en casos específicos, puedes utilizar un comentario para deshabilitar la revisión de tipos:

main.ts
// @ts-ignore: Desactivamos revisión temporalmente
function multiply(a: any, b: any) {
return a * b;
}

No es una solución ideal, pero puede ser útil al abordar situaciones particulares donde el uso de any sea inevitable.

#Configuracion de Typescript

En Deno se pueden configurar globalmente las reglas relacionadas con el tipado utilizando el archivo de configuración deno.json. Este archivo permite personalizar varias opciones, incluyendo las reglas sobre el uso de any.

Una alternativa para evitar la advertencia del tipo any es desactivar la opción noImplicitAny en el archivo deno.json.

deno.json
{
// ...
"compilerOptions": {
"noImplicitAny": false
}
}

Al aplicar esta configuración, Deno permitirá el uso del tipo any implícito, pero esta no es una práctica recomendada. Deshabilitar la opción noImplicitAny reduce los beneficios del tipado estático y puede resultar en código menos seguro y más propenso a errores.

Una solcion todavia más radical seria desactivar completarmente la opcion strict con el valor false:

deno.json
{
// ...
"compilerOptions": {
"strict": false
}
}

Aunque esta configuración elimina las restricciones de TypeScript, no se recomienda desactivar el modo strict, ya que limitaría la capacidad del compilador para detectar errores. Los expertos en TypeScript generalmente coinciden en que trabajar con strict activado es la mejor práctica para proyectos de cualquier escala.

#Especificar Tipos

La mejor forma de evitar advertencias e implementar un tipado seguro es asignar tipos explícitos a los parámetros. En el caso de nuestro ejemplo, podemos actualizar la función multiply:

main.ts
function multiply(a: number, b: number) {
return a * b;
}

Aquí hemos especificado que los parámetros a y b deben ser del tipo number. Si pasamos cualquier otro valor que no sea del tipo number como argumento, TypeScript arrojará un error de compilación.

multiply(5, "foo"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.

#Inferenia del Tipo de Retorno

Generalmente no es necesario declarar explícitamente el tipo de retorno de una función, ya que TypeScript puede inferirlo automáticamente con base en el valor que retorna. Ejemplo:

main.ts
function multiply(a: number, b: number) {
return a * b;
}

Aquí, TypeScript infiere que el valor de retorno es del tipo number porque el resultado proviene de la operación aritmética entre dos números.

Es posible especificar explícitamente el tipo de retorno utilizando anotaciones de tipo. Por ejemplo:

main.ts
function multiply(a: number, b: number): number {
return a * b;
}

En la mayoría de los casos, esto puede resultar innecesario si el tipo es claramente inferido por el compilador. De hecho, evitar anotaciones explícitas para el tipo de retorno puede hacer que tu código sea más limpio y legible.

Por otro lado, si necesitas verificar rápidamente el tipo de retorno de una función, puedes simplemente pasar el cursor sobre la función en tu IDE.

#Objetos Fuertemente Tipados

En TypeScript, también es posible definir objetos fuertemente tipados, lo cual resulta útil al trabajar con estructuras de datos complejas, como las que provienen del uso de APIs, donde el tipo y la estructura del objeto pueden ser desconocidos o no definibles a simple vista.

Al definir estructuras tipadas para objetos, también podemos aprovechar IntelliSense para explorar las propiedades y tipos asociados directamente en nuestro IDE, facilitando la escritura y comprensión de nuestro código.

#Definiendo Objetos

Supongamos que trabajamos con un objeto que tiene tres propiedades. Conforme creamos múltiples instancias similares a este objeto, puede suceder que algunas propiedades no estén asignadas o no haya claridad en los tipos que deberían contener. Para resolver este problema, podemos usar interfaces de TypeScript y establecer una estructura tipada.

Interfaces

Una interfaz en TypeScript te permite definir una "firma de tipos" que servirá como plantilla para tu objeto. La sintaxis es similar a la de un objeto regular, pero en lugar de claves y valores, especificamos claves junto con sus tipos asociados. Como convención, los nombres de las interfaces suelen comenzar con una letra mayúscula.

Considera el siguiente ejemplo:

// Definimos una interfaz que describe la estructura de un objeto
interface User {
id: number;
name: string;
email: string;
}

Aquí hemos definido un objeto User que debe tener tres propiedades:

  • id de tipo number.
  • name de tipo string.
  • email de tipo string.

Una vez definido, podemos usar la interfaz para garantizar que las variables asociadas sigan esta estructura.

const user: User = {
id: 1,
nombre: "Oliver Servín",
};

Mediante esta anotación explícita User para la variable user, TypeScript nos asegura que el objeto cumpla con el contrato impuesto por la interfaz. Si falta alguna propiedad definida en el User, o si asignamos un tipo incorrecto a alguna propiedad, TypeScript devolverá un error.

const user: User = {
id: 2,
name: "Darío Jiménez"
// Error: Falta la propiedad 'email' definida en la interfaz Usuario
};

Además, incluso si aún no asignamos valores a las propiedades del objeto, TypeScript identificará los tipos esperados al pasar el cursor sobre las propiedades, proporcionándonos documentación y autocompletado a través de IntelliSense.

#Tipos type

Además de las interfaces, TypeScript también permite usar la palabra clave type para definir estructuras similares. type puede utilizarse de forma casi idéntica a una interfaz para definir la "firma de tipos" de un objeto. Por ejemplo:

type User = {
id: number;
name: string;
email: string;
};

El uso de type y interface tiene muchas similitudes en la mayoría de los casos. Sin embargo, hay diferencias avanzadas:

  • Interfaces están diseñadas para ser extendidas o combinadas.
  • type está pensado para combinaciones más flexibles, como intersecciones y estructuras de uniones.

Para usos simples, como definir la estructura básica de un objeto, ambas opciones son equivalentes. Sin embargo, es importante elegir una estrategia consistente dentro de tus proyectos según tus preferencias o necesidades a largo plazo.

#Unión de Tipos (Union Types)

En TypeScript, puede surgir la necesidad de trabajar con objetos o funciones que acepten múltiples tipos de datos como argumentos. Para estos casos, podemos usar los Union Types, que se definen utilizando el operador de barra (|) y permiten combinar dos o más tipos diferentes.

#Usando Union Types

Al utilizar Union Types, definimos que una propiedad o variable puede aceptarse como uno de varios tipos especificados. Por ejemplo:

let result: string | number;
 
result = "éxito"; // Válido
result = 200; // Válido
result = true; // Error: El tipo 'boolean' no está asignado a 'string | number'

En este ejemplo, la variable result puede ser de tipo string o number. Pero si intentamos asignarle un valor del tipo boolean, TypeScript devolverá un error. Esto asegura que la variable acepte únicamente los tipos que fueron especificados en la unión.

Objectos

Cuando aplicamos Union Types a objetos, podemos definir que una propiedad acepta diferentes tipos de valores, adaptándose dinámicamente al uso deseado. Aquí un ejemplo:

interface ActiveUser {
id: number;
status: "active";
}
 
interface InactiveUser {
id: number;
status: "inactive";
}
 
type Usuario = ActiveUser | InactiveUser;
 
const usuario: User = {
id: 1,
status: "active"
};
 
console.log(user.status); // "active"
 
// Error en caso de un valor inválido no definido como parte de la unión:
user.status = "pending";
// Error: El tipo '"pending"' no se puede asignar a los valores permitidos por 'User'.

En este caso, la propiedad status solo acepta los valores active o inactive.

#Tipos Personalizados con type

Conforme los Union Types escalan en un proyecto, pueden llegar a ser difíciles de reutilizar o de mantener si se repiten a lo largo del código base. Para mejorar la organización y legibilidad, podemos crear tipos personalizados, lo que permite reutilizarlos más fácilmente en cualquier parte del proyecto.

Por ejemplo:

type UserStatus = "active" | "inactive";
 
interface User {
id: number;
status: UserStatus;
}
 
const usuario: User = {
id: 39,
estado: "active"
};
 
// Cambiando el estado:
user.status = "inactive"; // Válido
user.status = "pending";
// Error: El status sólo puede ser "active" o "inactive".

En este ejemplo, extraemos el tipo posible para el status con un tipo personalizado llamado UserStatus. Esto hace que el código sea más limpio y, además, mejora la reutilización de este tipo personalizado, ya que UserStatus podría ser utilizado en otras interfaces u objetos relacionados dentro del proyecto.

#Genéricos

Los genéricos son una herramienta clave en TypeScript cuando se trabaja con tipos dinámicos, manteniendo aún la seguridad que aporta el tipado estático. Son útiles cuando tenemos múltiples tipos y que deseamos reutilizar o combinar. En lugar de crear manualmente nuevos tipos o depender de los Union Types, los genéricos proporcionan una solución más eficiente y flexible.

#Fundamentos

Supongamos que trabajamos con dos tipos definidos y queremos crear un nuevo tipo basado en esos dos. Si tenemos muchos tipos en un proyecto, los Union Types pueden volverse poco manejables e ineficientes. En estos casos, podemos usar genéricos con un parámetro como T para definir tipos dinámicos.

Un genérico (<T>) funciona como un parámetro de entrada que representa el tipo. Funciona como un parámetro de una función, pero en lugar de pasar un valor, se pasa un tipo. Este tipo puede ser dinámico y reutilizable en otras partes del código.

Por ejemplo:

function identity<T>(value: T) {
return value;
}
 
// Llamamos a la función con diferentes tipos
const number = identity<number>(39); // T es "number"
const word = identity<string>("Hola"); // T es "string"

En este ejemplo, la función identity utiliza un genérico <T>, lo que nos permite aplicar distintos tipos como number o string, conservando aún el tipado estático.

#Genéricos y Promesas

Los genéricos son especialmente útiles en estructuras de datos como las Promesas. En JavaScript, las Promesas siempre devuelven un valor envuelto, pero no el valor directo. Por ejemplo, si una función async retorna un número, técnicamente devuelve una promesa de ese número (Promise<number>):

async function obtainData(): Promise<string> {
return "Datos recibidos";
}
 
const result = obtainData(); // resultado es de tipo: Promise<string>

Por defecto, TypeScript inferirá automáticamente el tipo de Promesa según el valor que retorna la función. Sin embargo, es recomendable declarar explícitamente el tipo de resultado utilizando genéricos, especialmente en funciones complejas.

async function obtainUser<T>(id: number): Promise<T> {
// Obtiene datos de un usuario con tipos dinámicos
const data: T = await apiCall<T>(`/users/${id}`);
return data;
}

En este caso, T representa un tipo dinámico que será definido en el momento en que se invoque la función apiCall, haciendo que la promesa sea de tipo Promise<T>. Este patrón es esencial al trabajar con APIs o datos donde el tipo puede variar.

#Tipos Incorporados y Librerías

Aunque los genéricos pueden ser poderosos, es recomendable minimizar la creación de tipos personalizados en lo posible. Suele ser mejor confiar en los tipos integrados o en los proporcionados por los autores de las librerías.

Si trabajamos en un desarrollo frontend, podemos usar los tipos incorporados como Document y HTMLElement. Sin embargo, al usar estos tipos en Deno, podríamos obtener errores indicando que el "tipo no fue encontrado". Esto se debe a que Deno no incluye por defecto los tipos destinados al DOM.

Referenciando Tipos del DOM

Para resolver este problema, existen dos soluciones.

Si las interfaces como Document o HTMLElement se utilizan únicamente en un archivo específico, podemos agregar un comentario de referencia para importar los tipos de la biblioteca DOM:

/// <reference lib="dom" />
const element: HTMLElement = document.getElementById("app");

Si estas interfaces se utilizan en todo el proyecto, es mejor configurar el archivo deno.json para incluir la librería:

deno.json
{
"compilerOptions": {
"lib": ["dom"]
}
}

Así, Deno tendrá acceso automático a los tipos de Document y HTMLElement en todo el proyecto, sin requerir referencias explícitas.

#Type Checking en Archivos JavaScript

Deno no solo es compatible con TypeScript, sino también con archivos JavaScript normales. Sin embargo, en JavaScript no contamos con tipado estático de forma predeterminada. Para habilitar el type checking en un archivo JavaScript, podemos usar el comentario especial // @ts-check al inicio del archivo. Esto indica que se debe analizar el archivo y validar los tipos.

main.js
// @ts-check
 
/** @type {string} */
let msg = "Hola mundo";
 
msg = 42;
// Error: El tipo 'number' no se puede asignar al tipo 'string'.

Esta técnica es útil si queremos aprovechar algunas de las características de TypeScript, como validaciones de tipo y autocompletado, mientras seguimos usando código JavaScript estándar. Es perfecta para proyectos donde se quiere obtener los beneficios del tipado sin recurrir a toda la sintaxis de TypeScript.