🔐 Programando tipos en Typescript - Parte II

En el post anterior comenzamos a definir nuevos tipos de datos dinámicamente en Typescript utilizando las herramientas del metalenguaje, principalmente con los tipos generics y el uso condicional de estos.

En este post observaremos otras utilidades que nos permiten extender nuestras definiciones de forma dinámica.

Los ejemplos están disponibles también en este espacio de repl.it.

Keyof

El operador keyof nos permite obtener una unión de las propiedades de un objeto.

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

type PersonProperties = keyof Person; // "name" | "age" | "isAlive" 

⚠️ Algo muy importante es que en Typescript las propiedades de un objeto son siempre coaccionadas a un string. Esto implica que si se tiene un número como propiedad Typescript lo interpretará como string. Para observarlo también he creado este repl.it.

Typeof e infer

El operador typeof de Javascript nos permite inferir el tipo de datos a partir de un valor. Dado que estamos operando sobre valores esta utilidad produce una variable.

let myName = "Raymundo";

let nameType = typeof myName; //string

Typescript añade un operador interno, infer, que opera de la misma manera que typeof en el contexto de un tipo de datos. Por ejemplo, usando infer podemos obtener el tipo de datos que una función retorna.

const getAnimal = () => ({ name: "perro", legs: 4 });

// Si T es una función: devuelve el tipo de datos que la función retorna
type MyReturnType<T> = T extends (...params: any) => infer R ? R : never;

// { name: string, legs: number }
type Animal = MyReturnType<typeof getAnimal>;

Si se observa correctamente la inferencia requiere también del uso del operador typeof. Esto se debe a que con él se puede obtener la firma de la función que determina su tipo, T, para que después pueda ser operado con infer mediante MyReturnType.

En este caso typeof sobre getAnimal retorna el tipo () => {name: string, legs: number} y al aplicar MyReturnType a este tipo obtenemos {name: string, legs: number }.

Acceso indexado de tipos

Typescript permite crear tipos a partir de propiedades de otros tipos.

Una forma de hacerlo es mediante el acceso a través de una propiedad específica

// string
type PersonName = Person["name"];
// number
type PersonAge = Person["age"];
// boolean
type PersonIsAlive = Person["isAlive"];

También podemos obtener la unión de todos los tipos de datos que son manejados por un objeto mediante el acceso a través de keyof.

// Todos los tipos de datos para las propiedades de Person
// string | number | boolean
type PersonTypes = Person[keyof Person];

Al operar con arreglos de datos Typescript tiene un operador especial, number, que permite obtener la definición del tipo de los elementos del arreglo.

const animals = [
  { name: "dog", legs: 4 },
  { name: "human", legs: 2 },
];

// { name: string, legs: number }
type Animal = typeof animals[number];

const beings = [
  { name: "dog", legs: 4 },
  { name: "broccoli", arms: 16 }
];

// { name: "string", legs: number, arms?: undefined } | { name: "string", arms: number, legs?: undefined }
type Beings = typeof beings[number];

Validando el registro de datos para un objeto

Con lo mencionado anteriormente podemos escribir una pequeña función que validará el tipo de datos antes de realizar la actualización de un objeto.

function updateData<T>(prop: keyof T, value: T[keyof T], seed: T): T {
  if (typeof seed[prop] !== typeof value) {
    throw "tipo de datos incorrecto";
  }
  return {
    ...seed,
    [prop]: value,
  }
}

const seed: Person = {
  name: "Ray",
  age: 37,
  isAlive: true, // y coleando
};

// T como Person es inferido por Typescript mediante `seed`
const upd = updateData("name", "Raymundo", seed);

// Updated { name: 'Raymundo', age: 37, isAlive: true }
console.log("Updated", upd);

// Error: Argument of type location is not assignable
const incorrect = updateData("location", "Brussels", seed);

// esto produce un error: "tipo de datos incorrecto"
const wrong = updateData("age", "37", seed);

Como se observa la función updateData opera sobre un tipo de datos genérico T y acepta solamente propiedades que estén defininas en T, así como valores con el tipo de datos que les corresponde de acuerdo a su definición.