TypeScript, menos TypeScript
A menos que haya estado escondido debajo de una roca durante los últimos años (y seamos realistas, esconderse debajo de una roca a veces parece lo correcto), probablemente haya oído hablar de TypeScript y probablemente lo haya usado. TypeScript es un superconjunto sintáctico de JavaScript que agrega, como su nombre indica, escritura al lenguaje de secuencias de comandos favorito de la web.
TypeScript es increíblemente poderoso, pero a menudo es difícil de leer para los principiantes y conlleva la sobrecarga de necesitar un paso de compilación antes de poder ejecutarse en un navegador debido a la sintaxis adicional que no es válida para JavaScript. Para muchos proyectos esto no es un problema, pero para otros puede obstaculizar la realización del trabajo. Afortunadamente, el equipo de TypeScript ha habilitado una forma de verificar el tipo de JavaScript básico usando JSDoc.
Configurando un nuevo proyecto
Para poner TypeScript en funcionamiento en un nuevo proyecto, necesitará NodeJS y npm. Comenzamos a crear un nuevo proyecto y ejecutando npm init. Para los propósitos de este artículo, usaremos VShttps://code.visualstudio.comCode como nuestro editor de código. Una vez que todo esté configurado, necesitaremos instalar TypeScript:
npm i -D typescript
Una vez realizada la instalación, debemos decirle a TypeScript qué hacer con nuestro código, así que creamos un nuevo archivo llamado tsconfig.json
y agregamos esto:
{ "compilerOptions": { "target": "esnext", "module": "esnext", "moduleResolution": "node", "lib": ["es2017", "dom"], "allowJs": true, "checkJs": true, "noEmit": true, "strict": false, "noImplicitThis": true, "alwaysStrict": true, "esModuleInterop": true }, "include": [ "script", "test" ], "exclude": [ "node_modules" ]}
Para nuestros propósitos, las líneas importantes de este archivo de configuración son las opciones allowJs
y checkJs
ambas configuradas en true
. Estos le dicen a TypeScript que queremos que evalúe nuestro código JavaScript. También le hemos dicho a TypeScript que verifique todos los archivos dentro de un /script
directorio, así que creemos eso y un nuevo archivo llamado index.js
.
Un ejemplo sencillo
Dentro de nuestro archivo JavaScript recién creado, creemos una función de suma simple que toma dos parámetros y los suma:
function add(x, y) { return x + y;}
Bastante simple, ¿verdad? add(4, 2)
devolverá 6
, pero debido a que JavaScript se escribe dinámicamente, también puedes llamar a add con una cadena y un número y obtener algunos resultados potencialmente inesperados:
add('4', 2); // returns '42'
Eso no es lo ideal. Afortunadamente, podemos agregar algunas anotaciones JSDoc a nuestra función para decirles a los usuarios cómo esperamos que funcione:
/** * Add two numbers together * @param {number} x * @param {number} y * @return {number} */function add(x, y) { return x + y;}
No hemos cambiado nada sobre nuestro código; simplemente agregamos un comentario para decirles a los usuarios cómo se debe usar la función y qué valor se debe esperar que devuelva. Hemos hecho esto utilizando JSDoc @param
y @return
anotaciones con tipos establecidos entre llaves ( {}
).
Intentar ejecutar nuestro fragmento incorrecto anterior genera un error en VS Code:
En el ejemplo anterior, TypeScript lee nuestro comentario y lo verifica por nosotros. En TypeScript real, nuestra función ahora es equivalente a escribir:
/** * Add two numbers together */function add(x: number, y: number): number { return x + y;}
Al igual que usamos el number
tipo, tenemos acceso a docenas de tipos integrados con JSDoc, incluidos string, object, Array y muchos otros, como HTMLElement
y MutationRecord
más.
Un beneficio adicional de usar anotaciones JSDoc sobre la sintaxis patentada de TypeScript es que brinda a los desarrolladores la oportunidad de proporcionar metadatos adicionales en torno a argumentos o definiciones de tipos al proporcionarlos en línea (con suerte, fomentando hábitos positivos de autodocumentar nuestro código).
También podemos decirle a TypeScript que las instancias de ciertos objetos pueden tener expectativas. A WeakMap
, por ejemplo, es un objeto JavaScript integrado que crea una asignación entre cualquier objeto y cualquier otro dato. Este segundo dato puede ser cualquier cosa de forma predeterminada, pero si queremos que nuestra WeakMap
instancia solo tome una cadena como valor, podemos decirle a TypeScript lo que queremos:
/** @type {WeakMapobject, string} */const metadata = new WeakMap();
const object = {};const otherObject = {};
metadata.set(object, 42);metadata.set(otherObject, 'Hello world');
Esto genera un error cuando intentamos configurar nuestros datos 42
porque no son una cadena.
Definiendo nuestros propios tipos
Al igual que TypeScript, JSDoc nos permite definir y trabajar con nuestros propios tipos. Creemos un nuevo tipo llamado Person
que tiene name
y age
propiedades hobby
. Así es como se ve en TypeScript:
interface Person { name: string; age: number; hobby?: string;}
En JSDoc, nuestro tipo sería el siguiente:
/** * @typedef Person * @property {string} name - The person's name * @property {number} age - The person's age * @property {string} [hobby] - An optional hobby */
Podemos usar la @typedef
etiqueta para definir nuestro tipo name
. Definimos una interfaz de llamada Person
con las propiedades requeridas name
(una cadena)) y age
(un número), más una tercera propiedad opcional llamada hobby
(una cadena). Para definir estas propiedades, usamos @property
(o la @prop
clave abreviada) dentro de nuestro comentario.
Cuando elegimos aplicar el Person
tipo a un nuevo objeto usando el @type
comentario, obtenemos verificación de tipo y autocompletar al escribir nuestro código. No solo eso, también se nos avisará cuando nuestro objeto no cumpla con el contrato que hemos definido en nuestro archivo:
Ahora, completar el objeto borrará el error:
A veces, sin embargo, no queremos un objeto completo para un tipo. Por ejemplo, es posible que queramos proporcionar un conjunto limitado de opciones posibles. En este caso, podemos aprovechar algo llamado tipo de unión:
/** * @typedef {'cat'|'dog'|'fish'} Pet */
/** * @typedef Person * @property {string} name - The person's name * @property {number} age - The person's age * @property {string} [hobby] - An optional hobby * @property {Pet} [pet] - The person's pet */
En este ejemplo, hemos definido un tipo de unión llamado Pet
que puede ser cualquiera de las opciones posibles de 'cat'
, 'dog'
o 'fish'
. Cualquier otro animal en nuestra área no está permitido como mascota, por lo que si caleb
arriba intentamos adoptar uno 'kangaroo'
en su hogar, obtendríamos un error:
/** @type {Person} */const caleb = { name: 'Caleb Williams', age: 33, hobby: 'Running', pet: 'kangaroo'};
Esta misma técnica se puede utilizar para mezclar varios tipos en una función:
/** * @typedef {'lizard'|'bird'|'spider'} ExoticPet */
/** * @typedef Person * @property {string} name - The person's name * @property {number} age - The person's age * @property {string} [hobby] - An optional hobby * @property {Pet|ExoticPet} [pet] - The person's pet */
Ahora nuestro tipo de persona puede tener a Pet
o ExoticPet
.
Trabajar con genéricos
Puede haber ocasiones en las que no queramos tipos estrictos y rápidos, sino un poco más de flexibilidad mientras seguimos escribiendo código consistente y fuertemente tipado. Introduzca tipos genéricos. El ejemplo clásico de una función genérica es la función de identidad, que toma un argumento y se lo devuelve al usuario. En TypeScript, se ve así:
function identityT(target: T): T { return target;}
Aquí, estamos definiendo un nuevo tipo genérico ( T
) y le decimos a la computadora y a nuestros usuarios que la función devolverá un valor que comparte un tipo con cualquier argumento target
. De esta manera, aún podemos pasar un número, una cadena o un HTMLElement
y tener la seguridad de que el valor devuelto también es del mismo tipo.
Lo mismo es posible usando la notación JSDoc usando la @template
anotación:
/** * @template T * @param {T} target * @return {T} */function identity(target) { return x;}
Los genéricos son un tema complejo, pero para obtener documentación más detallada sobre cómo utilizarlos en JSDoc, incluidos ejemplos, puede leer la página del Compilador de cierre de Google sobre el tema.
Tipo de fundición
Si bien la escritura segura suele ser muy útil, es posible que descubra que las expectativas integradas de TypeScript no funcionan del todo para su caso de uso. En ese tipo de casos, es posible que necesitemos convertir un objeto a un nuevo tipo. Un caso en el que esto podría ser necesario es cuando se trabaja con detectores de eventos.
En TypeScript, todos los detectores de eventos toman una función como devolución de llamada donde el primer argumento es un objeto de tipo Event
, que tiene una propiedad, destino, que es un archivo EventTarget
. Este es el tipo correcto según el estándar DOM, pero muchas veces la información que queremos obtener del objetivo del evento no existe EventTarget
, como la propiedad de valor que existe en HTMLInputElement.prototype
. Eso hace que el siguiente código no sea válido:
document.querySelector('input').addEventListener(event = { console.log(event.target.value);};
TypeScript se quejará de que la propiedad value
no existe EventTarget
aunque nosotros, como desarrolladores, sabemos muy bien que input
tiene un archivo value
.
Para que podamos decirle a TypeScript que sabemos que event.target
será un HTMLInputElement
, debemos convertir el tipo del objeto:
document.getElementById('input').addEventListener('input', event = { console.log(/** @type {HTMLInputElement} */(event.target).value);});
Colocarlo event.target
entre paréntesis lo diferenciará de la llamada a value
. Agregar el tipo antes del paréntesis le indicará a TypeScript que queremos decir que event.target
es algo diferente de lo que normalmente espera.
Y si un objeto en particular está siendo problemático, siempre podemos decirle a TypeScript que un objeto debe @type {any}
ignorar los mensajes de error, aunque esto generalmente se considera una mala práctica a pesar de ser útil en caso de apuro.
Terminando
TypeScript es una herramienta increíblemente poderosa que muchos desarrolladores utilizan para optimizar su flujo de trabajo en torno a estándares de código consistentes. Si bien la mayoría de las aplicaciones utilizarán el compilador integrado, algunos proyectos pueden decidir que la sintaxis adicional que proporciona TypeScript se interpone en su camino. O tal vez simplemente se sienten más cómodos apegándose a los estándares en lugar de estar atados a una sintaxis ampliada. En esos casos, los desarrolladores aún pueden obtener los beneficios de utilizar el sistema de tipos de TypeScript incluso mientras escriben JavaScript básico.
Deja un comentario