Entendiendo los Módulos en JavaScript y Deno
-
Oliver Servín
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:
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.
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:
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álidoid = 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.
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
.
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
.
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:
// @ts-ignore: Desactivamos revisión temporalmentefunction 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
.
{ // ... "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
:
{ // ... "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
:
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:
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:
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 objetointerface User { id: number; name: string; email: string;}
Aquí hemos definido un objeto User
que debe tener tres propiedades:
-
id
de tiponumber
. -
name
de tipostring
. -
email
de tipostring
.
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álidoresult = 200; // Válidoresult = 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álidouser.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 tiposconst 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:
{ "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.
// @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.