Utilizzo di Option con fp-ts nel codice TypeScript

Jan 30 2023
L'opzione è Data Structure (Container), che consente di memorizzare i dati con due possibili stati: il valore è assente (Nessuno) o il valore è presente (Alcuni). Possiamo usare Option per rappresentare un valore facoltativo in un oggetto in JavaScript.

L'opzione è Data Structure (Container), che consente di memorizzare i dati con due possibili stati: il valore è assente (None) o il valore è presente (Some). Possiamo usare Option per rappresentare un valore facoltativo in un oggetto in JavaScript.

Ad esempio, supponiamo di avere un tipo di oggetto persona e un tipo di oggetto indirizzo:

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 ...
};

Per accedere a addressLine2 all'interno dell'oggetto indirizzo , intuitivamente vogliamo fare qualcosa di simile a addressOption.addressLine2.

Tuttavia, non è possibile farlo perché il valore di address è racchiuso in Option , quindi è necessario scartare l' indirizzo da Option e quindi accedere al valore di addressLine2 .

Inoltre, anche addressLine2 dovrebbe essere racchiuso in Option perché il valore potrebbe essere vuoto. Quindi, se siamo in grado di ottenere il valore non vuoto di address , potremmo fare lo stesso per addressLine2 usando option.fromNullable(address.addressLine2).

I seguenti frammenti di codice mostrano due approcci per ottenere il valore non vuoto di address al fine di ottenere il valore di 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 ...
};

Per spiegare brevemente .map , ti aiuta a scartare il valore all'interno del contenitore ed eseguire alcune trasformazioni con i dati. (Sembra familiare con la funzione .map quando lavori con Array in JS, giusto? :))

Oh aspetta ... se controlli il tipo di addressLine2Option , è Option < Option < string >>, il valore addressLine2 è racchiuso in un'Opzione racchiusa in un'altra Opzione ! Ma quello che vogliamo è solo < Option < string >>!

Se guardi la firma del tipo ( la firma del tipo è MOLTO IMPORTANTE nella programmazione funzionale ) di " map " in Option , che è f: (a: A -> b: B) -> Option<a: A> -> Option< b: B>

Funziona in questo modo: prima scarta i dati "a" da Option<a: A> , fornisce il valore "a" come input a f , trasforma "a" in "b", quindi avvolge il risultato "b" con Opzione e restituirlo. (Nota: se Option<a: A> è None, la funzione f non verrà attivata e verrà restituito il valore di origine (None). )

Questo è il motivo per cui prima otteniamo Option < Option < string >> invece di < Option < string >> !

Fortunatamente, possiamo risolverlo facilmente! Poiché entrambi i livelli sono Option, possiamo utilizzare .flatten per rimuovere il livello esterno:

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);

Infine, dovremmo restituire il valore di addressLine2 con il valore predefinito "" se il valore è vuoto utilizzando 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);

Cordiali saluti, userei pipe per semplificare i dati e il flusso per la composizione delle funzioni (fidati di me, questa è la versione finale!).

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);

Immagina che ci siano più livelli nidificati di proprietà facoltative nell'oggetto. Ad esempio, il tipo di addressLine2 non è più una stringa, ma viene modificato in

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)),

Maggiori informazioni su fp-ts .

Grazie per aver letto
Spero che questo articolo ti piaccia e che ci guadagni qualcosa. Spero che ora sia meno scoraggiante per te imparare la programmazione funzionale :)

Ka Hin