Introducción a TypeScript

Publicado: 2020-10-29

Los primeros lenguajes con los que he programado son C y Java. Con estos lenguajes, cada vez que defines una variable tienes que especificar su tipo. Si intenta asignarle un valor de otro tipo, el compilador se quejará:

 // main.c int main() { int x = "two"; return 0; } > cc main.c -o main // warning: initialization makes integer from pointer without a cast

El problema es que, debido a mi falta de experiencia, muchas de las quejas que recibí del compilador parecían crípticas y complejas. Por eso, al final, miré al compilador como una herramienta que limitaba mi creatividad, cuando en realidad se supone que es un socio que está ahí para ayudar.

Gif de Bob Esponja chocando la computadora

Más adelante en mi carrera, comencé a usar algunos lenguajes de programación sin escritura fuerte, como JavaScript o PHP. Pensé que eran geniales: era extremadamente fácil crear prototipos de cosas rápidamente sin tener que lidiar con un compilador quisquilloso.

Como ya sabrás, los lenguajes de programación en los que se basa WordPress son PHP y JavaScript. Esto significa que probablemente esté acostumbrado a codificar sus complementos y temas sin un compilador que le vigile las espaldas. Solo eres tú, tus habilidades y tu creatividad. Bueno, y errores como este:

 const user = getUser( userId ); greet( user.name ); // Uncaught TypeError: user is undefined

Si está harto de errores undefined en su código, es hora de agregar un compilador a su flujo de trabajo. Echemos un vistazo a qué es TypeScript y cómo nos permite mejorar la calidad de nuestro software en varios órdenes de magnitud.

¿Qué es TypeScript?

TypeScript es un lenguaje de programación basado en JavaScript que se creó con el objetivo de agregar escritura fuerte y estática. Los tipos de TypeScript nos permiten describir la forma de nuestros objetos y variables, lo que da como resultado un código mejor documentado y más sólido. El propio TypeScript se encargará de validar todo lo que hagamos.

Tal como se diseñó, TypeScript es un superconjunto de JavaScript. Esto significa que cualquier código escrito en JavaScript simple es, por definición, también TypeScript válido. Pero lo contrario no es cierto: si usa funciones específicas de TypeScript, el código resultante no es JavaScript válido hasta que lo transpila.

Cómo funciona TypeScript

Para entender cómo funciona TypeScript vamos a utilizar su Playground , un pequeño editor donde podemos escribir código TypeScript y ver qué nos dice el compilador al respecto.

Al ser un superconjunto de JavaScript, escribir código TypeScript es extremadamente fácil. Básicamente el siguiente código JavaScript:

 let user = "David"; let age = 34; let worksAtNelio = true; console.log( user, age, worksAtNelio );

también es código TypeScript. Puedes copiarlo y pegarlo en el TypeScript Playground y verás que compila. Entonces, ¿qué tiene de especial? Sus tipos, por supuesto. En JavaScript, puede hacer lo siguiente:

 let user = "David"; let age = 34; let worksAtNelio = true; user = { name: "Ruth" }; console.log( user, age, worksAtNelio );

pero eso provocará un error en TypeScript. Pruébelo en Playground y verá el siguiente error:

 Type '{ name: string; }' is not assignable to type 'string'.

Entonces, ¿de qué se trata todo eso?

TypeScript puede inferir el tipo de una variable automáticamente. Eso significa que no necesitamos decirle explícitamente "oye, esta variable es una cadena" (o un número, o un valor booleano, o lo que sea); en cambio, mira el valor que se le ha dado por primera vez e infiere su tipo basado en eso.

Gif de hombre teniendo una gran gran idea

En nuestro ejemplo, cuando definimos la variable user , le asignamos la cadena de texto "David" , por lo que TypeScript sabe que el user es (y siempre debe ser) una cadena . El problema es que un poco más adelante intentamos cambiar el tipo de nuestra variable de user asignándole un objeto que tiene una sola propiedad ( name ) cuyo valor es la cadena "Ruth" . Claramente, este objeto no es una cadena , por lo que TypeScript se queja y nos dice que la asignación no se puede realizar.

Hay dos rutas posibles para arreglar esto:

 // Option 1 let user = "David"; user = "Ruth"; // OK. // Option 2 let user = { name: "David" }; user = { name: "Ruth" };

Seamos explícitos a la hora de definir los tipos de nuestras variables

Pero la inferencia de tipos solo está ahí para ayudarnos. Si queremos, podemos decirle explícitamente a TypeScript el tipo de una variable:

 let user: string = "David"; let age: number = 34; let worksAtNelio: boolean = true; console.log( user, age, worksAtNelio );

que genera exactamente el mismo resultado que los tipos inferidos por TypeScript. Ahora, esto plantea la pregunta: si TypeScript puede inferir tipos automáticamente, ¿por qué necesitamos esta característica?

Por un lado, especificar tipos de forma explícita puede servir para documentar mejor nuestro código (que se aclarará aún más en la siguiente sección). Por otro lado, nos permite resolver las limitaciones del sistema de inferencia de tipos. Sí, has leído bien: hay casos en los que la inferencia no es muy buena y lo mejor que puede hacer TypeScript es decirnos que “bueno, no sé qué se supone que es exactamente esta variable; ¡Supongo que puede ser cualquier cosa!

Considere el siguiente ejemplo:

 const getName = ( person ) => person.name; let david = { name: "David", age: 34 }; console.log( getName( david ) ); // Parameter 'person' implicitly has an 'any' type.

En este caso, estamos definiendo una función getName que recibe un parámetro y devuelve un valor. Observe cuántas cosas estamos asumiendo en una función tan simple: estamos esperando un objeto ( person ) con al menos una propiedad llamada name . Pero realmente no tenemos ninguna garantía de que quien llame a esta función la usará correctamente. Pero incluso si lo hacen, todavía no sabemos cuál es el tipo resultante de esta función. Claro, puede suponer que un nombre será una cadena , pero solo lo sabe porque es un ser humano y comprende el significado de esa palabra. Pero mira los siguientes ejemplos:

 getName( "Hola" ); // Error getName( {} ); // Returns undefined getName( { name: "David" } ); // Returns a string getName( { name: true } ); // Returns a boolean

cada vez que llamamos a la función, ¡obtenemos un resultado diferente! Entonces, a pesar de que TypeScript nos cuida las espaldas, todavía nos encontramos con problemas clásicos de JavaScript: esta función puede recibir cualquier cosa y puede devolver cualquier cosa.

Gif gracioso de un bebe muy triste

La solución, lo adivinó, es anotar explícitamente la función.

Defina sus propios tipos

Pero antes de hacerlo, veamos otra característica adicional de TypeScript: tipos de datos personalizados. Hasta ahora, casi todos nuestros ejemplos usaban tipos básicos como cadena , número , booleano , etc., que creo que son bastante fáciles de entender. También hemos visto estructuras de datos más complejas, como objetos:

 let david = { name: "David", age: 34 };

pero no prestamos mucha atención a su tipo como lo infiere TypeScript, ¿verdad? Todo lo que hemos visto es un ejemplo de una asignación no válida debido a una falta de coincidencia de escritura:

 let user = "David"; user = { name: "Ruth" }; // Type '{ name: string; }' is not assignable to type 'string'.

que de alguna manera insinuó que el tipo del objeto era " {name:string;} ", que puede leerse como "este es un objeto con una propiedad llamada nombre de tipo string ". Si así es como se definen los tipos de objetos, entonces lo siguiente debería ser un TypeScript válido:

 let david: { name: string, age: number } = { name: "David", age: 34 };

y lo es de hecho. Pero estoy seguro de que estará de acuerdo en que es cualquier cosa menos conveniente. Afortunadamente, podemos crear nuevos tipos en TypeScript.

En general, un tipo personalizado es básicamente un tipo TypeScript regular con un nombre personalizado:

 type Person = { name: string; age: number; }; let david: Person = { name: "David", age: 34 };

Genial, ¿eh? Ahora que tenemos el tipo Person , podemos anotar fácilmente nuestra función getName y especificar claramente el tipo de nuestro parámetro de entrada:

 const getName = ( person: Person ) => person.name;

¡Y esta simple actualización le da a TypeScript mucha información! Por ejemplo, ahora puede inferir que el tipo de resultado de esta función es una cadena , porque sabe con certeza que el atributo de name de un objeto Persona también es una cadena :

 let david: Person = { name: "David", age: 34 }; let davidName = getName( david ); davidName = 2; // Type 'number' is not assignable to type 'string'.

Pero, como siempre, puede anotar explícitamente el tipo de resultado si lo desea:

 const getName = ( person: Person ): string => person.name;

Más cosas con tipos de TypeScript…

Finalmente, quería compartir con ustedes un par de hazañas interesantes de las que pueden beneficiarse si definen sus propios tipos en TypeScript.

TypeScript es muy exigente cuando se trata de asignar valores a las variables. Si ha definido explícitamente el tipo de una determinada variable, solo puede asignar valores que coincidan exactamente con ese tipo. Veámoslo con varios ejemplos:

 type Person = { name: string; age: number; }; let david: Person = { name: "David", age: 34 }; // OK let ruth: Person = { name: "Ruth" }; // Property 'age' is missing in type '{ name: string; }' but required in type 'Person' let toni: Person = { name: "Toni", age: 35, gender: "M" }; // Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'. Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.

Como puede ver, una variable de Persona solo acepta objetos que cumplan completamente con el tipo de Persona . Si faltan algunos atributos ( ruth no tiene age ) o hay más atributos que los incluidos en el tipo ( toni tiene gender ), TypeScript se quejará y desencadenará un error de discrepancia de tipos.

Pero, si estamos hablando de invocar funciones, ¡las cosas son diferentes! Los tipos de argumentos en una función no son un requisito exacto, pero especifican la interfaz mínima que deben cumplir los parámetros. Lo sé, lo sé, eso es demasiado abstracto. Veamos el siguiente ejemplo, que creo que aclarará las cosas:

 type NamedObject = { name: string; }; const getName = ( obj: NamedObject ): string => obj.name; const ruth = { name: "Ruth" } getName( ruth ); // OK const david = { name: "David", age: 34 }; getName( david ); // OK const toni = { firstName: "Toni" }; getName( toni ); // Argument of type '{ firstName: string; }' is not assignable to parameter of type 'NamedObject'. Property 'name' is missing in type '{ firstName: string; }' but required in type 'NamedObject'.

Como puedes ver, hemos redefinido la función getName como algo un poco más genérico: ahora toma un objeto NamedObject , es decir, un objeto que debe tener un atributo llamado name de tipo string . Usando esta definición, vemos como ruth y david cumplen perfectamente con estos requisitos (ambos tienen un atributo de name ), pero toni no, ya que no tiene el atributo esperado name .

Conclusión

TypeScript es un lenguaje de programación que amplía JavaScript al agregar definiciones de tipos estáticos. Esto nos permite ser mucho más precisos a la hora de definir los datos con los que trabajamos y, lo que es más importante, nos ayuda a detectar errores antes.

El costo de integrar TypeScript en una pila de desarrollo es relativamente pequeño y se puede hacer gradualmente. Dado que todo el código JavaScript es, por definición, TypeScript, cambiar de JavaScript a TypeScript es automático: puede agregar tipos y embellecer su código paso a paso.

Si te gustó esta publicación y quieres saber más, compártela y házmelo saber en la sección de comentarios a continuación.

Imagen destacada de King's Church International en Unsplash.