Frank Hliva - PlutonKrea s.r.o

Front-end Developer & majiteľ

rokov skúsenosti

rokov prax

roky React

Úvod / Články / SafeAccess - lepšia náhrada za operátor elvis

SafeAccess - lepšia náhrada za operátor elvis

Publikoval Frank Hliva 2019-11-17 02:07:00 print comments
typescript ramda elvis safeaccess

Pretože ma už otravovalo neustále opakovanie kódu pri čítaní hodnoty z property objektu urobil som si na to libku: SafeAccess.ts

Kód je z StdLib, konkrétne modul učený na kontrolu existencie properties a ich bezpečné čítanie... Síce je to len taká kravinka, ale používam ju veľmi často a tak som to chcel mať napísané poriadne.

pôvodne som na tento účel používal funkcie z ramdy ako:

R.prop, R.propOr, R.path, R.pathOr

Neskôr som ich obalil a trochu vylepšil, ale nakoniec som sa tých ramda funkcií úplne zbavil a naimplementoval som si lepšie riešenie. Potreboval som totiž pridať nejakú vlastnú funkcionalitu a tá sa k ramdovskému riešeniu nedala pridať.

Ale vráťme sa najprv na začiatok a ukážme si ako to celé funguje. Dajme tomu že chcem čítať hodnoty pár vnorených objektov napr. state.form.ui.list.loading a nevieme či tie propsy objekt naozaj obsahuje:

Klasické jskové riešenie sú takéto hnusné špagety (ja tomu hovorím schody):

const loading = (
    state &&
    state.form &&
    state.form.ui &&
    state.form.ui.list &&
    state.form.ui.list.loading
) || null;

Odstrašujúci príklad ako by sa kód nikdy nemal písať (po bitke je každý generál, ale ja som túto konštrukciu tiež používal). Zbytočne sa tam opakuje kód a keď náhodou chcem zmeniť state za iný identifikátor, musím editovať rovno 5 riadkov po sebe. Taký kód sa ťažko udržiava, nehovoriac o tom, že to porušuje princíp DRY.

Našťrastie som objavil knižnicu Ramda. Ramda to rieši takto:

R.pathOr(null, ['form', 'ui', 'list', 'loading'], state)

Optroti pôvodnému zápisu je to naprvý pohľad krásne, ale na druhý to má tiež ďaleko od dokonalosti. Prečo tam musia byť 4 stringové literály? a medzi nimi ešte čiarky? Nedalo by sa to viac učesať?

Nehovoriac o zápise ten je ešte otravnejší. Takže ma napadlo to trochu vylepšiť:

const loading = safeAccess(state).getOr('form.ui.list.loading', null);

V prípade defaultnej null hodnoty ešte jeden (nepodstatný) syntaktický cukor:

const loading = safeAccess(state).getOrNull('form.ui.list.loading');

Pozn. okrem toho že ramdácke riešenie je trochu neprehľadné, vačšia nevýhoda je, že nerozlišuje medzi undefined hodnotou a medzi neexistujúcou property... Ja naopak používam typ TOption<T> (nespolieham sa na undefined) takže takéto rozlišovanie je v mojom riešení implicitné.

Je možné to celé zavolať aj ako jednu funkciu v celku len safeAccess treba vymeniť za modul AccessPath všetky funkcie majú obidve alternatívy:

const loading = safeAccess(state).getOr('form.ui.list.loading', null);

je to isté ako:

const loading = AccessPath.getOr(state, 'form.ui.list.loading', null);

Ale teraz k typu TOption. Ak nám ide o bezpečnosť môžme pracovať s TOption:

const maybeProfileImage = safeAccess(state).tryGet('user.profile.image');
switch (maybeProfileImage.kind) {
	case "Some": {
		const { value: profileImage } = maybeProfileImage;
		return this.loadProfileImage(profileImage);
	}
	case "None": return this.loadDefautProfileImage();
}

pozn: všetky funckie vracajúce TOption začínajú prefixom try konvenciu som prevzal z môjho obľúbeného jazyka F#

Práca s typom TOption je síce o niečo ukecanejšia, ale o to bezpečnejšia, pretože vždy musíme testovať hodnotu, inak sa k nej ani nedostaneme.

Samozrejme funguje aj pôvodná ramdovská verzia s arrayom tá síce neni až taká pekná, ale občas sa hodí (neskôr si ukážeme načo):

const loading = AccessPath.getOr(state, ['form', 'ui', 'list', 'loading'], null);

Obidve verzie sa dajú takto pekne kombinovať:

const users = isActive ? 'activeUsers' : 'inactiveUsers';
const loading = AccessPath.getOr(state, ['datalists.users.items', listName, 'data'], null);

lenže stáva sa že nechcem čítať property ale funkciu (alebo ich kombinácie) a na to slúži:

Accessor.func(názov funkcie: string, arg1: any, arg2: any, ..., argn: any) - je to - variadická funkcia a tu máme príklad ako sa to používa, musí sa použiť pole a časť, ktorá reprezentuje funkciu nahradíme Accessor.func

const defaultValue = {
	...
}
const uuid = "55d02288-9fe6-476e-adce-10695960f4c2"
const loading = safeAccess(state).getOr(['window.webscokets', Accessor.func('get', uuid)], defaultValue);

Rovnako vieme bezpečne indexovať polia (prípadne objekty) len Accessor.func vymeníme za Accessor.index

Zápis je takýto: Accessor.index(názov property obsahujúcej index: string, index: any)

const defaultValue = ...
const loading = safeAccess(state).getOr(['datalists.users.items.activeUsers', Accessor.index('data', 5)], defaultValue);

A tiež získavanie číslených hodnôt (celé číslo):

const counter = safeAccess(state).getInt('form.ui.list.counter', -1);

alebo reálne číslo:

const zoom = safeAccess(state).getFloat('form.ui.list.zoom', 0.5);

Pričom sa kontroluje nie len existencia property, ale aj to či ide o číslo (musí sa jednať o číslo, alebo string obsahujúci číslo). Ak je to číselný string, tak sa skonvertuje na integer (resp float). A v opačnom prípade sa vráti defaultná hodnota. Samozrejme v JS je typ integer aj typ float jeden a ten istý typ number, akurát pri getInt sa použije parseInt (parsuje sa iba celočíselná časť) a pri getFloat sa použije parseFloat (parsuje aj desatinné miesta). Ukážka:

forms = forms || {};
forms.ageForm = {
	nameInput: $('#user-input'),
	ageInput: $('#age-input')
};


const coord = (coord: "left" | "top") => safeAccess(window).getInt(['forms.loginForm.nameInput', Accessor.func("offset"), coord], 0);
const
	x = coord("left"),
	y = coord("top");
const age = safeAccess(window).getInt(['window.loginForm.ageInput', Accessor.func("val")], -1);

pozn. najlepšie je kód písať tak aby sme safeAccess nepotrebovali žiaľ v rozsiahlejšom projekte v dynamickom jazyku sa tomu občas neubránime.

Elvis operátor (?.)

Na záver si ešte povedzme že Typescript konečne pridal operátor elvis. Ktorý slúži presne nato načo slúže aj táto libka. Akurát ako všetko v JS neni to úplne dotiahnuté do konca a tak sa vraciame spať k libke. Ale aspoň to základné čítanie funguje perfektne:

const loading = form?.ui?.list?.loading || null;

A teraz to najdôležitejšie. Prečo sa ten operátor volá Elvis? Pri troche fantázie vzdialene pripomína Elvisov účes, aj keď priznám sa, že mne sa viac podobá na Miley Cyrus. Náhoda? Nemyslím :)