🔑 Programando tipos en Typescript - Parte I

Typescript es un sistema de tipos de datos que opera alrededor de Javascript. Nos ofrece la capacidad de validar la definición del código que escribimos en este lenguaje.

No opera en tiempo de ejecución, es decir, no es capaz de validar los datos que son recibidos o exportados desde una función en Javascript.

La principal característica es que refuerza las definiciones del código en Javascript y permite evitar errores de acceso, definición y lógica.

Un sencillo ejemplo se puede observar a continuación

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

function getName(p: Person) {
  return p.firstname; //error: Property firstname does not exist on type Person
} 

En este ejemplo Typescript nos ayuda a utilizar correctamente la definición de la entidad sobre la que queremos operar.

Nota: todos los ejemplos se encuentran en este tablero de repl.it.


Typescript permite también obtener nuevos tipos de datos a partir de tipos de datos. Esto se puede realizar de diversas formas.

Extendiendo tipos con genéricos

La forma básica de obtener nuevos tipos de datos es mediante el uso de genéricos. Con estos podemos extender definiciones y utilizarlas de diversas formas dependiendo de su contexto.

Por ejemplo extenderemos nuestra definición anterior para Person con un argumento genérico para la dirección, que en este caso puede ser sólo una cadena de caracteres o algo más complejo.

// El argumento genérico `AddressType` define el tipo de datos de la propiedad `address`
type Person<AddressType> = {
  name: string;
  age: number;
  address: AddressType;
}

// Este tipo de Persona tiene una dirección sencilla
type PersonAddressSingleLine = Person<string>;

// Este tipo de datos define a una dirección con varias propiedades
type Address = {
  street: string;
  number: number;
  zipcode: string;
  country: string;
}

// Este tipo de Persona tiene una dirección más completa
type PersonComplexAddress = Person<Address>;

function getSimpleAddress(p: PersonAddressSingleLine) {
  return p.address;
}

function getPersonsCountry(p: PersonComplexAddress) {
  return p.address.country;
}

De esta forma podemos reutilizar la definición de Persona que hemos creado y adaptarla a las necesidades de nuestros casos de uso.

Si intentamos asignar algún argumento que no cumple con la definición del tipo de datos Typescript se encargará de marcar el error indicando una incompatibilidad.

Restricciones para genéricos

Los argumentos genéricos no sólo funcionan para definir tipos, también pueden ser utilizados para funciones y para crear otros tipos mediante comparaciones. Estas comparaciones se realizan mendiante el uso de la palabra reservada extends, la cual provee límites sobre los que un tipo genérico puede operar.

// Definimos una función en donde devolvemos el código postal
// de cualquier entidad con una dirección del tipo `Address`.
function getAnyonesZipcode<T extends { address: Address }>(p: T) {
  console.log("Zipcode", p.address.zipcode);
  return p.address.zipcode;
}

// Una organización también cuenta con una dirección
type Organization = {
  name: string;
  address: Address;
}

// Dame el código postal de una organización
const getOrganizationZipCode = (o: Organization) => getAnyonesZipcode(o);

// Dame el código postal de una persona
const getPersonsZipcode = (p: PersonComplexAddress) => getAnyonesZipcode(p);

En el ejemplo anterior la función getAnyonesZipcode cuenta con una restricción definida mediante T extends {address: Address} que indica que puede operar sobre todo tipo T que tenga una propiedad address que sea del tipo Address (definido previamente).

De esta forma podemos construir funciones que operan sobre tipos de datos específicos, Organization y PersonAddressMultiProperties, las cuáles cumplen con dicha propiedad.

Si no cumplimos con dicha restricción Typescript nos lo indicará adecuadamente:

// error: Types of property address are incompatible
const getSimpleZipcode = (p: PersonAddressSingleLine) => getAnyonesZipcode(p);

Los tipos que declaramos también pueden ser uniones de varios tipos, por ejemplo:

type AddressTypes = { address: string } | { address: Address };

Este nuevo tipo también puede ser usado para limitar argumentos genéricos y utilizar las propiedades de la entidad de forma correcta.

function someAddress<T extends AddressTypes>(p: T) {
  if ("string" === typeof p.address) {
    console.log("Simple Address:", p.address);
  } else {
    console.log("Complex Address => { country:", p.address.country, ", ...}");
  }
}

someAddress({ address: "Calle nueva 1" });
someAddress({
  address: {
    street: "Calle Norte",
    number: 1,
    zipcode: "701190-A",
    country: "México"
  }
});

// error: tipos incompatibles
someAddress("algo no bueno");
someAddress({ address: 345 }) 

La parte más interesante de extends es que también puede ser usada para crear un nuevo tipo evaluando un argumento genérico de otro tipo

// Un tipo de datos que indica si el genérico cumple con la restricción
type IsAddressable<T> = T extends { address: Address } ? true : false;

// true
type IsOrgAddressable = IsAddressable<Organization>;

//false
type IsSimpleAddressable = IsAddressable<PersonAddressSingleLine>;

⚠️ Lamentablemente estos tipos no son de mucha utilidad fuera de una definición de los datos ya que no pueden ser usados para evaluar datos en tiempo de ejecución, es decir, no podemos corroborar con estas definiciones si nuestra información cumple o no con ciertas características.

Aún así podemos usar esta propiedad de extends para crear tipos que definan arreglos de elementos que contienen direcciones:

// define un tipo que cuando recibe una entidad con una dirección
// la convierte en un tipo que implementa un arreglo
type ManyAddresses<T> = T extends AddressTypes ? T[] : never;

// Arreglo de direcciones simples
type ManySimple = ManyAddresses<{ address: string }>;

// Arreglo de direcciones complejas
type ManyComplex = ManyAddresses<{ address: Address }>;

// Una función que retorna un arreglo de direcciones a partir de una semilla
function getManyAddresses<T extends AddressTypes>(seed: T): ManyComplex | ManySimple {
  if ("string" === typeof seed.address) {
    return [
      {
        address: seed.address + + "Simple one"
      },
      {
        address: seed.address + + "Simple two"
      },
    ]
  }

  return [
    {
      address: {
        street: seed.address.street,
        number: 1,
        zipcode: "701190-A",
        country: "México"
      }
    },
    {
      address: {
        street: seed.address.street,
        number: seed.address.number + 100,
        zipcode: "701190-B",
        country: "México"
      }
    }
  ]
}

console.log(getManyAddresses({
  name: "Vieja fábrica de clavicornios",
  address: {
    street: "Calle Norte",
    number: 1,
    zipcode: "701190-A",
    country: "México"
  }
}));

Como se puede observar en estos ejemplos el sistema de tipos de Typescript es una herramienta que permite definir y derivar nuevos tipos de datos a partir de otros ya definidos.

En otro artículo observaremos más utilidades que nos permiten seguir extendiendo nuestras definiciones.

Comments:

🔑 Programando tipos en Typescript – Parte II – Blog de Raymundo Vásquez Ruiz -

[…] el post anterior comenzamos a definir nuevos tipos de datos dinámicamente en Typescript utilizando las herramientas […]