Introducción a TypeScript

Publicada en WordPress.

Mira nuestro vídeo

Existe una versión mejor de tu web

Comparte este artículo

Cuando aprendí a programar, lo hice con lenguajes como C y Java. Con estos lenguajes, cada vez que definías una variable debías especificar el tipo de la misma y si intentabas asignarle un valor de otro tipo, el compilador se quejaba y te llamaba de todo menos bonito:

// 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, por mi falta de experiencia, muchas de las quejas que recibía eran extrañas y difíciles de entender, con lo que al final veía al compilador como un freno a mi creatividad y no como un compañero que está ahí para ayudarme.

Luego fui jugando con más lenguajes de programación y descubrí que había algunos que no te obligaban a especificar tipos. Eran lenguajes mucho más libres, con menos restricciones. JavaScript o PHP son un par de ejemplos, pero hay muchos otros. Y de repente pensé: «vaya, esto es muchísimo más cómodo; no tengo al pesado de turno dando por el saco porque las cosas no le cuadran».

Como ya sabes, precisamente los lenguajes de programación sobre los que se sustenta WordPress son PHP y JavaScript. Por lo tanto, lo más probable es que estés acostumbrado a programar con total libertad, confiando en tus habilidades y experiencia. ¡Ah! Y probablemente también estés acostumbrado a resolver un montón de incidencias del palo:

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

Así que si estás harto de que tu código pete y que casi siempre acaben apareciendo problemas relacionados con undefined, es hora de añadir un compilador a tu flujo de trabajo. Vamos a ver qué es TypeScript y cómo nos permite mejorar varios órdenes de magnitud la calidad de nuestro software a cambio de pagar un precio muy pequeño.

Qué es TypeScript

TypeScript es un lenguaje de programación basado en JavaScript que nace con el objetivo de añadirle tipado fuerte y estático. Los tipos de TypeScript permiten describir la forma que van a tener nuestros objetos y variables, resultando en un código mejor documentado y más robusto, ya que el propio TypeScript se encargará de validar todo lo que hacemos.

Tal cual está diseñado, TypeScript es un superconjunto de JavaScript. Esto quiere decir que un código cualquiera escrito en JavaScript plano es, por definición, código escrito también en TypeScript. Pero lo contrario no es cierto: si usas cosas específicas de TypeScript, el resultado no es JavaScript hasta que no lo transpiles.

Cómo funciona TypeScript

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

Al tratarse de un superconjunto de JavaScript, escribir código en TypeScript es extremadamente sencillo. Básicamente, el siguiente código JavaScript:

let user = "David";
let age = 34;
let worksAtNelio = true;

console.log( user, age, worksAtNelio );

también es, por definición, código TypeScript. Puedes probar de pegarlo en el Playground de TypeScript y verás que no da ningún problema. Entonces, ¿qué tiene de especial? Los tipos, por supuesto. En JavaScript, tú puedes hacer esto:

let user = "David";
let age = 34;
let worksAtNelio = true;

user = { name: "Ruth" };

console.log( user, age, worksAtNelio );

pero en TypeScript, no. Si lo pruebas en el Playground, verás que te devuelve el siguiente error:

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

¿Qué está pasando exactamente aquí?

TypeScript es capaz de inferir el tipo de una variable automáticamente, con lo que no necesitamos indicarle siempre de forma explícita qué tipo va a tener: cuando ve una asignación, es capaz de inferir su tipo.

En nuestro ejemplo, cuando hemos definido la variable user le hemos asignado la cadena de texto "David", así que TypeScript sabe que user es (y siempre deberá ser) un string. El problema está en que un poco más adelante intentamos cambiar el tipo de nuestra variable user asignándole un objeto que tiene una única propiedad (name) cuyo valor es el string "Ruth". Está claro que este objeto no es un string, así que TypeScript se queja y nos dice que no se puede realizar la asignación.

Si lo arreglamos de cualquiera de las siguientes formas:

// Option 1
let user = "David";
user = "Ruth"; // OK.

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

veremos que TypeScript ya no se queja, porque el tipo de la primera asignación es el mismo que el tipo de la segunda asignación.

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

Si queremos, TypeScript nos permite explicitar el tipo de una variable:

let user: string = "David";
let age: number = 34;
let worksAtNelio: boolean = true;

console.log( user, age, worksAtNelio );

lo cual es totalmente equivalente a las conclusiones que había llegado el sistema de inferencia de tipos de TypeScript. Entonces, ¿por qué tenemos esta opción? Por dos motivos.

Por un lado, especificar los tipos de forma explícita puede servir para documentar mejor nuestra aplicación (lo cual quedará aún más claro cuando saltemos a la siguiente sección). Por otro lado, nos permite solventar una limitación de la inferencia de tipos, y es que hay casos en los que esa inferencia no es demasiado buena y lo mejor que puede hacer TypeScript es decirnos que, «bueno, esta variable puede ser cualquier cosa».

Considera 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. Si te fijas, en esta función estamos suponiendo un montón de cosas: estamos asumiendo que lo que recibiremos será un objeto (person) que tendrá, como mínimo, una propiedad llamada name. Pero en realidad no tenemos ninguna garantía de que la persona que invoque esa función realmente nos pase un objeto con ese atributo. Y, si lo hace, tampoco no tenemos ni idea de qué tipo tendrá ese atributo name (aunque nosotros, leyendo el nombre, podamos dar por sentado que será un string), con lo cual no sabemos qué tipo devuelve la función getName:

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

Es decir, a pesar de estar bajo el paraguas de TypeScript, seguimos con las dudas de JavaScript: esta función puede recibir cualquier cosa y puede devolver cualquier cosa. Así que la solución está en anotarla especificando explícitamente el tipo de la variable de entrada y el tipo que queremos de salida.

Define tus propios tipos

Antes de ver cómo anotar la función getName, veamos otra funcionalidad adicional de TypeScript: crear nuestros propios tipos de datos. Hasta ahora, en casi todos nuestros ejemplos hemos usado tipos básicos: string, number, boolean, etc. Pero también hemos visto que podíamos asignar un objeto a una variable:

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

pero hemos obviado comentar cuál es el tipo que TypeScript ha inferido. Si recuerdas, cuando creamos una variable de tipo string y le asginamos un objeto, TypeScript nos devolvió el siguiente error:

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

Es decir, parece que el segundo tipo es «{ name: string; }» o, dicho de una forma un poco más clara, «un objeto que tiene una propiedad llamada name de tipo string». Con lo que, siguiendo el ejemplo, podríamos explicitar el tipo de la variable david de la siguiente forma:

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

lo cual es correcto, pero bastante incómodo. La solución pasa por crear un nuevo tipo en TypeScript. O, dicho de una forma menos rimbombante, «dar un nombre a ese tipo de objeto»:

type Person = {
  name: string;
  age: number;
};

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

Con este nuevo tipo, ahora es extremadamente sencillo anotar nuestra función getName y especificar claramente cuál es el tipo que nuestro parámetro de entrada debe cumplir:

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

Además, y de regalo, ahora TypeScript es capaz de inferir que el tipo del resultado será un string, porque sabe que el atributo name de un objeto Person es de tipo string:

let david: Person = { name: "David", age: 34 };
let davidName = getName( david );

davidName = 2;
// Type 'number' is not assignable to type 'string'.

aunque de haber querido, también podríamos haber especificado exactamente el tipo de salida:

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

Jugando un poco con los tipos…

Finalmente, quería comentarte un par de curiosidades de las que puedes beneficiarte si defines tus propios tipos en TypeScript.

TypeScript es muy exigente a la hora de controlar las asignaciones de valores a variables. Si has definido explícitamente el tipo de una cierta variable, únicamente podrás asignarle valores que coincidan exactamente con dicho 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; sex: string; }' is not assignable to type 'Person'. Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.

Como puedes ver, si defino una variable como Person y no especifico todos sus atributos (en ruth no le he puesto age) o pongo atributos de más (a toni le he metido gender), TypeScript se queja y nos dice que el tipo esperado no coincide con el tipo del valor dado.

Ahora bien, esto no es así cuando invocamos a una función. Los tipos de los argumentos de una función especifican lo que, como mínimo, deben cumplir los parámetros con los que las invocamos, pero no deben ser exactamente iguales. Como esta suena un poco más abstracto, veámoslo con otro ejemplo:

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 mismo acepta como parámetro un objeto de tipo NamedObject, el cual tiene como único requisito el tener un atributo llamado name de tipo string. A partir de ahí, vemos como los objetos ruth y david cumplen perfectamente con esa definición (ambos tienen el atributo name), pero toni, no, ya que su único atributo (firstName) no es el atributo esperado (name).

Conclusión

TypeScript es un lenguaje de programación que extiende a JavaScript añadiéndole un tipado fuerte y estático. 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 aún, nos ayuda a detectar errores antes de que los vea el usuario final.

El coste de integrar TypeScript en un stack de desarrollo es relativamente pequeño y se puede hacer de forma gradual. Dado que todo código JavaScript es, por definición, TypeScript, podemos anotar poco a poco el código que tengamos existente y pasándolo de JavaScript básico a TypeScript vitaminado.

Si te ha gustado esta entrada y quieres saber más, compártela y dímelo en los comentarios.

Imagen destacada de King’s Church International en Unsplash.

Deja una respuesta

No publicaremos tu correo electrónico. Los campos obligatorios están marcados con: •

He leído y acepto la Política de privacidad de Nelio Software

Al marcar la casilla de aceptación estás dando tu legítimo consentimiento para que tu información personal se almacene en SiteGround y sea usada por Nelio Software con el propósito único de publicar aquí este comentario. Contáctanos para corregir, limitar, eliminar o acceder a tu información.