Los tipos predicados son una característica sintáctica interesante en TypeScript. Aunque aparecen en el mismo lugar que las anotaciones de tipo de retorno, parecen más como afirmaciones cortas que como las típicas anotaciones. Esto le brinda un mayor control sobre la comprobación de tipos.

Con la publicación de TypeScript 5.5, trabajar con tipos predicados se ha vuelto más intuitivo ya que puede inferirlos automáticamente en muchos casos. Sin embargo, si estás navegando por bases de código un poco más antiguas, es más probable que encuentres tipos predicados escritos a mano.

En este artículo, exploraremos brevemente qué son los tipos predicados y por qué son útiles. Comencemos viendo el problema que resolven.

El Problema

Creo que la mejor manera de entender la utilidad de los tipos predicados es notando los problemas que surgen cuando no los tenemos:

function isString(value: unknown): boolean {
  return typeof value === "string";
}

function padLeft(padding: number | string, input: string) {
  if (isString(padding)) {
    return padding + input;
        //   ^
        // string | number
  }
  return " ".repeat(padding) + input; // Opps type error here
                 //   ^
                 // string | number
}

Aquí, el tipo de retorno de isString se establece en boolean, y lo usamos en una función llamada padLeft para agregar relleno a la izquierda de una cadena de entrada. El padding puede ser una cadena dada o un número específico de caracteres de espacio.

Es posible que te estés preguntando por qué codifiqué el tipo de retorno como boolean. Esto es para ilustrar el problema. Si no agregas ninguna anotación de tipo de retorno y utilizas la última versión de TypeScript, no notarás ningún problema aquí. Por ahora, aguanta conmigo: discutiremos las diferencias relacionadas con la versión en breve.

La función funcionará sin problemas en tiempo de ejecución, pero TypeScript no puede realizar ningún estrechamiento de tipo con isString. Como resultado, el tipo de padding sigue siendo string | number tanto dentro como fuera de la instrucción if. Esto genera un conflicto con la expectativa de repeat para su primer argumento, lo que provoca el error de tipo.

La solución: introducir predicados de tipo

Incluso si no estás familiarizado con el término predicado, es probable que los hayas utilizado antes. Los predicados en programación son simplemente funciones que devuelven un booleano para responder una pregunta de sí/no. Varios métodos de matriz integrados en JavaScript, como filter, find, every y some, utilizan predicados para ayudar en la toma de decisiones.

Los predicados de tipo son una forma de hacer que los predicados sean más útiles para el estrechamiento de tipo. Podemos solucionar el problema utilizando un predicado de tipo como tipo de retorno:

function isString(value: unknown): value is string {
  return typeof value === "string";
}

Aquí el predicado de tipo es value is string. Está diciendo tres cosas:

  • La función es un predicado. De esta manera, TypeScript mostrará un error si intentas devolver cualquier cosa que no sea un valor booleano.

  • Si devuelve true, entonces value es de tipo string.

  • Si devuelve false, entonces value no es de tipo string.

Los predicados de tipo te permiten crear guías de tipo personalizadas. Las guías de tipo son comprobaciones lógicas que te permiten refinar los tipos a tipos más específicos, es decir, estrecharlos. Así que la función de arriba también es un guardia de tipo personalizado.

Aquí está el código completo:

function isString(value: unknown): value is string {
  return typeof value === "string";
}

function padLeft(padding: number | string, input: string) {
  if (isString(padding)) {
    return padding + input;
        //   ^
        // string
  }
  return " ".repeat(padding) + input;
                 //   ^
                 // number
}

En este caso, TypeScript reduce correctamente el tipo de padding dentro del bloque if y fuera de él.

Ahora veamos brevemente cómo funcionaban los predicados de tipo antes de TypeScript 5.5 y qué ha mejorado esta versión.

Predicados de Tipo Antes de TypeScript 5.5

En nuestro ejemplo anterior, si no especificamos ningún tipo de retorno, se infiere como boolean:

function isString(value: unknown) {
  return typeof value === "string";
}

function padLeft(padding: number | string, input: string) {
  if (isString(padding)) {
    return padding + input;
        //   ^
        // string | number
  }
  return " ".repeat(padding) + input; // Opps type error here
                 //   ^
                 // string | number
}

Por lo tanto, tenemos el mismo error que cuando escribimos manualmente el tipo de retorno boolean. Aquí está el enlace del entorno de prueba de TypeScript para el fragmento de código anterior. Ve y posa el cursor sobre las funciones o variables para una mejor sensación de los tipos. Luego ve y ve cómo la escritura de la predicada de tipo resuelve el problema.

Si no especificamos la predicada de tipo, usar métodos como filter también puede resultar en una detección de tipo incorrecta:

function isString(value: unknown) {
  return typeof value === "string";
}

const numsOrStrings = [1, 'hello', 2, 'world'];
//      ^
//    strings: (string | number)[]

const strings = numsOrStrings.filter(isString);
//      ^
//    strings: (string | number)[]

Ahora veamos cómo TypeScript 5.5 mejora la situación.

Predicadas de Tipo Después de TypeScript 5.5

Una de las características más destacadas de TypeScript 5.5 es que puede inferir predicadas de tipo analizando el cuerpo de la función. Así que si estás usando TypeScript 5.5 o una versión posterior, no tienes que escribir la predicada de tipo como el tipo de retorno de isString. TypeScript lo hace por ti, y el código como lo que ven en el ejemplo de abajo funciona perfectamente:

function isString(value: unknown) {
  return typeof value === "string";
}

function padLeft(padding: number | string, input: string) {
  if (isString(padding)) {
    return padding + input;
        //   ^
        // cadena
  }
  return " ".repeat(padding) + input; // Error de tipo aquí
                 //   ^
                 // número
}

const numsOrStrings = [1, 'hello', 2, 'world'];

const strings = numsOrStrings.filter(isString);
//      ^
//    cadenas: string[]

const numbers = numsOrStrings.filter((v) => !isString(v));
//      ^
//    números: number[]

Aún no he encontrado una situación en la que esté insatisfecho con la inferencia automática de los predicados de tipo. Si encuentras alguna, siempre puedes escribir los tuyos propios manualmente.

Estudio adicional

En este artículo, exploramos brevemente los predicados de tipo en TypeScript. Si estás interesado en aprender más y comprender los casos particulares, aquí están las guías oficiales:

¡Gracias por leer! ¡Nos vemos la próxima vez!

El fondo de la portada está obtenido de Mona Eendra en Unsplash.