Usando Option com fp-ts em seu código TypeScript

Jan 30 2023
A opção é Data Structure (Container), que ajuda você a armazenar dados com dois estados possíveis: o valor está ausente (None) ou o valor está presente (Some). Podemos usar Option para representar um valor opcional em um Object em JavaScript.

A opção é Data Structure (Container), que ajuda você a armazenar dados com dois estados possíveis: o valor está ausente (None) ou o valor está presente (Some). Podemos usar Option para representar um valor opcional em um Object em JavaScript.

Por exemplo, digamos que temos um tipo de objeto de pessoa e um tipo de objeto de endereço:

type Address = {
  addressLine1: string;
  addressLine2?: string;
};

type Person = {
  name: string;
  age: number;
  address?: Address;
};

const getAddressLine2 = (person: Person) => {
  // To get the address line 2,
  // Need to check whether the value is empty (null or undefined or "")
  if (person.address && person.address.addressLine2) {
     return person.address.addressLine2;
  }
  // Return an empty string as the default value;
  return "";
};

import * as option from 'fp-ts/Option';

const getAddressLine2 = (person: Person) => {
  /*
    Step 1: create an Option container and wrap the data inside Option becasue address can be empty
    Use the Option constructor "option.fromNullable"
  */
  const addressOption = option.fromNullable(person.address);
  // Extract the value of addressLine2 from addressOption ...
};

Para acessar o addressLine2 dentro do objeto address , intuitivamente queremos fazer algo como addressOption.addressLine2.

No entanto, não poderíamos fazer isso porque o valor de address está encapsulado em Option , portanto, precisamos desempacotar o endereço de Option e acessar o valor de addressLine2 .

Além disso, addressLine2 também deve ser agrupado em Option porque o valor pode estar vazio. Portanto, se formos capazes de obter o valor não vazio de address , poderíamos fazer o mesmo para addressLine2 usando option.fromNullable(address.addressLine2).

Os trechos de código a seguir exibem duas abordagens para obter o valor não vazio de address para obter o valor de addressLine2 :

import * as option from 'fp-ts/Option';

const getAddressLine2 = (person: Person) => {
  /*
    Step 1: create an Option container and wrap the data inside Option becasue address can be empty
    Use the Option constructor "option.fromNullable"
  */
  const addressOption = option.fromNullable(person.address);

  /*
    Step 2: Extract the value of addressLine2 from addressOption 
    Approach 1 -
    Use "option.isSome" to extract the value
  */
  if (option.isSome(addressOption)) {
    // The address value can be obtained by .value
    const address = addressOption.value;
    
    // Wrap the addressLine2 with Option
    const addressLine2Option = option.fromNullable(address.addressLine2);
  }
  // Return the value of addressLine2 ...
};
import * as option from 'fp-ts/Option';

const getAddressLine2 = (person: Person) => {
  /*
    Step 1: create an Option container and wrap the data inside Option becasue address can be empty
    Use the Option constructor "option.fromNullable"
  */
  const addressOption = option.fromNullable(person.address);
  /*
    Step 2: Extract the value of addressLine2 from addressOption
    Approach 2 - 
    Use "option.map" to extract the value and wrap the addressLine2 with Option
  */
 const addressLine2Option = option.map((address) => option.fromNullable(address.addressLine2))(addressOption);

 // Return the value of addressLine2 ...
};

Para explicar brevemente o .map , ele ajuda a desempacotar o valor dentro do contêiner e realizar alguma transformação com os dados. (Parece familiar com a função .map quando você trabalha com Array em JS, certo? :))

Oh espere… se você verificar o tipo de addressLine2Option , é Option < Option < string >>, o valor addressLine2 é encapsulado em uma Option encapsulada em outra Option ! Mas o que queremos é apenas< Option < string >>!

Se você olhar a assinatura de tipo ( assinatura de tipo é MUITO IMPORTANTE em Programação Funcional ) de “ map ” em Option , que é f: (a: A -> b: B) -> Option<a: A> -> Option< b:B>

Funciona assim: Primeiro ele desempacota os dados “a” de Option<a:A> , alimenta o valor “a” como uma entrada para f , transforma “a” em “b”, então agrupa o resultado “b” com Opção e devolvê-lo. (Observação: se Option<a: A> for None, a função f não seria acionada e o valor de origem (None) seria retornado.)

É por isso que anteriormente obtemos Option < Option < string >> em vez de < Option < string >> !

Felizmente, podemos corrigi-lo facilmente! Como ambas as camadas são Option, podemos usar .flatten para remover a camada externa:

const addressLine2Option = option.map((address) => option.fromNullable(address.addressLine2))(addressOption);
 const addressLine2OptionForReal = option.flatten(addressLine2Option);

const addressLine2Option = option.chain((address) => option.fromNullable(address.addressLine2))(addressOption);

Por fim, devemos retornar o valor de addressLine2 com o valor padrão “” se o valor estiver vazio usando option.getOrElse :

import * as option from 'fp-ts/Option';

const getAddressLine2 = (person: Person) => {
  /*
    Step 1: create an Option container and wrap the data inside Option becasue address can be empty
    Use the Option constructor "option.fromNullable"
  */
  const addressOption = option.fromNullable(person.address);
  /*
    Step 2: Extract the value of addressLine2 from addressOption
    Approach 2 - 
    Use "option.chain" to extract the value and wrap the addressLine2 with Option and then flatten the result
  */
 const addressLine2Option = option.chain((address: Address) => option.fromNullable(address.addressLine2))(addressOption);
  /*
    Last Step: Return the value of addressLine2 with the default value "" if empty
  */
  const result = option.getOrElse(() => "")(addressLine2Option);
  return result;
};

const personA = {
  name: "A",
  age: 20,
  address: {
    addressLine1: "hehe",
    addressLine2: "haha",
  }
}

// You should get "haha"
getAddressLine2(personA);

Para sua informação, eu usaria pipe para simplificar dados e fluxo para composição de função (confie em mim, esta é a versão final!).

import * as option from 'fp-ts/Option';
import {pipe, flow} from 'fp-ts/function';

type Address = {
  addressLine1: string;
  addressLine2?: string;
};

type Person = {
  name: string;
  age: number;
  address?: Address;
};

const getAddressLine2Final = flow(
  option.fromNullableK((person: Person) => person.address),
  option.chain(address => option.fromNullable(address.addressLine2)),
  option.getOrElse(() => '')
);

const personA = {
  name: 'A',
  age: 20,
  address: {
    addressLine1: 'hehe',
    addressLine2: 'haha',
  },
};

// You should get "haha"
pipe(personA, getAddressLine2Final);

Imagine que há mais níveis aninhados de propriedades opcionais no objeto. Para instâncias, o tipo de addressLine2 não é mais uma string, mas é alterado para

type AddressLine2 = {
  country?: {
    city?: string;
  }
}

person.address && person.address.addressLine2 && 
person.address.addressLine2 && person.address.addressLine2.country && 
person.address.addressLine2.country.city

option.chain(addressLine2 => option.fromNullable(address.country)),
option.chain(country => option.fromNullable(country.city)),

Mais sobre fp-ts .

Obrigado por ler
Espero que goste deste artigo e ganhe algo com ele. Eu desejo que seja menos assustador agora para você aprender Programação Funcional :)

Ka Hin