La importancia de deliminar los datos a retornar

TypeScript

TypeScript, como es sabido por todos, es un superconjunto de JavaScript, pero con tipos. Esto es una gran ventaja, ya que es el compilador quien se encarga de verificar que los argumentos que se pasan a una función correspondan con el tipo de datos de sus parámetros.

Igualmente, cuando una función retorna un valor, es el compilador quien se encarga de validar que la variable que recibe dicho retorno sea del mismo tipo, o, cuando no se ha indicado el tipo de la variable, el compilador lo deduce tomando el tipo de dato retornado por la función. De igual manera, cuando tratamos de acceder a una propiedad o método de un objeto, nuevamente es el compilador quien se encarga de ello.

Hasta aquí todo bien, pero, ¿qué sucede en aquellos casos donde el valor de retorno corresponde correctamente con el tipo de datos, pero no con el valor que se esperaba?

Hace ya algún tiempo, varios colegas y yo estuvimos conversando sobre este tema. Algunos defendían la postura de solo definir el tipo de dato para el retorno, mientras que otros (me incluyo) preferían delimitar qué es lo que la función debería retornar.

Aunque no recuerdo exactamente qué hacía la función, daré un ejemplo simplista (muy simplista) que muestra el tema en discusión: retornar un false en vez de un boolean.

Supongamos que tenemos una función que crea una instancia de una clase Persona y la función cliente hace algo con el objeto, si este es retornado, por ejemplo, imprimir el valor de sus propiedades.

class Person {
  constructor(private name: string, private age: number) {}

  get Name() {
    return this.name;
  }

  get Age() {
    return this.age;
  }
}

const getPersonFromDb = async (id: number): Promise<Person | false> => {
    // Todo el código
    // ...
    // la creación del objeto
    const person new = Person("John", 25);

    // proceso
    return person
};

(async () => {
  const person = await getPersonFromDb(1);

  person && console.log(person.Name, person.Age);
})();

En la discusión, mi propuesta era algo similar a la función getPersonFromDb. Como podemos observar, la función retorna un Promise<Person | false>. Nota que también tenía gestión de excepciones; el false no era por temas de error. Es decir, la función debería retornar un objeto de Person o, en su defecto, retornar false.

Las personas que estaban en contra indicaban que la firma de la función era lo mismo que Promise<Person | boolean>, y definitivamente no lo es. Vamos por partes.

Si cambiamos la firma, el código no compila, como podemos observar en la siguiente imagen:

alt text

Ok, podemos modificar el código y utilizar un “if” para realziar la comparación., con lo cual cambiaríamos la línea

person && console.log(person.Name, person.Age);

por algo así

if (person) {
  console.log(person.Name, person.Age);
}

El código, con los cambios, quedaría así:

...
(async () => {
    const person = await getPersonFromDb(1);

    if (person) {
        console.log(person.Name, person.Age);
    }
})();

Listo, solucionado, oh, no, sorpresa, ahora el código se resalta en rojo en el editor:

alt text

y si tratamos ejecutar, nos muestra esto por la consola

alt text

bien, esto podemos solucionarlo eliminando “| boolean”, es decir, la firma de la función quedaría así:

const getPersonFromDb = async (id: number): Promise<Person>

Ok, ahora, qué hacemos con el return? qué retornamos cuando no tenemos un objeto Persona?

Una posible solución sería dejar nuevamente el boolean y verificar el tipo de dato retornado

class Person {
  constructor(private name: string, private age: number) {}

  get Name() {
    return this.name;
  }

  get Age() {
    return this.age;
  }
}

const getPersonFromDb = async (id: number): Promise<Person | false> => {
  // Todo el código
  // ...
  // la creación del objeto

  if (!datos) {
    // caso hipotético, en el cual no se puede crear el objeto
    console.log("No se puede generar el objeto....");
    return false;
  }

  const person = new Person("John", 25);

  // proceso
  return person;
};

const isPerson = (obj: any): obj is Person => {
  return obj instanceof Person;
};

(async () => {
  const person = await getPersonFromDb(1);

  if (isPerson(person)) {
    console.log(person.Name, person.Age);
  } else {
    console.log("No person found");
  }
})();

O podríamos utilizar un Promise.reject(), pero, en el caso que comento, no era una excepción lo que se estaba generando, simplemente aún cuando no teníamos un error no se contaba con todo lo necesario para generar el Objeto, se mostraba un mensaje por la consula y se retornaba un false, lo cual era válido para las reglas de negocio del módulo en cuestión.

En resumen, esto

const getPersonFromDb = async (id: number): Promise<Person | boolean>

no es lo mismo que esto

const getPersonFromDb = async (id: number): Promise<Person | false>

Nota final: esto no aplica para todos los casos, siempre debemos tomar en cuenta las reglas de negocio.