3. diel - Interface (rozhranie) v TypeScriptu
V predchádzajúcom cvičení, Riešené úlohy k 1.-2. lekciu v TypeScriptu, sme si precvičili získané skúsenosti z predchádzajúcich lekcií.
V minulej lekcii, Riešené úlohy k 1.-2. lekciu v TypeScriptu , sme sa pozreli na základy OOP v TypeScriptu.
Dnes sa pozrieme na jednu z najpraktickejších, a ak pracujete s väčším
množstvom dát, tak i najproblematickejších konštrukcií, ktoré
TypeScript do JavaScriptu prináša -
interface
. Ak ste nikdy nezavadili o typové programovacie jazyky,
bude pre vás táto téma zo začiatku možno trochu mätúce. Nebojte,
sľubujem, že s praxou sa to zlepší.
Čo interface robí?
Interface čiže rozhranie je akási zmluva programátora s programom, že premenná bude obsahovať dáta alebo metódy v požadovanom tvare. Ak je váš kód používaný vo väčšom tíme alebo ho používa niekto úplne iný, pomáhajú mu rozhranie porozumieť, čo vaše metódy vyžadujú na vstupe. Ak nedodržíte správny tvar objektu, váš editor, ak touto funkciou disponuje, vám napovie čo je s vaším kódom zle. Tým sa dá docieliť bezpečnej komunikácie medzi niekoľkými objektmi v programe.
Chápem, že do začiatku vám to veľa nehovorí. To je úplne v poriadku. Poďme sa pozrieť na príklad.
Založte si súbor karta.ts
a do neho napíšte:
interface Produkt { jmeno: string; popis: string; cena: number; skladem: boolean; }
Toto je interface
produktu, ktorý môže obsahovať napr.
Nejaký e-shop. Ako je vidieť, objekt produkt bude vždy obsahovať vlastnosti
jmeno
, popis
, cena
a informáciu či je
skladem
. Žiadnu z týchto vlastností nie je možné vynechať.
Zároveň nám interface
stanovuje, aký typ vlastnosti majú.
Teraz ho skúsime použiť.
Implementácia interface
Ak chceme implementovať interface
v nejakej premennej,
vykonáme to pridaním dvojbodky za názov premennej, ako je vidieť na
príklade:
const plysak: Produkt = { jmeno: 'Plyšový medvěd', popis: 5, cena: 223, skladem: true }
Kód vyššie vypíše chybu. Nižšie je ukážka z môjho Visual Studia Code:
Ako je vidieť, dostal som vynadané, pretože vlastnosť popis
nie je kompatibilný s typom number
. A aby mi to program ešte viac
uľahčil, tak mi napovie, ako má celý objekt vyzerať. Ak nemáte editor,
ktorý by bol schopný strážiť kód za chodu, skúste aplikáciu
skompilovať. Kompiluje príkazom v príkazovom riadku:
tsc karta.ts
výsledok:
C:\Users\David\Desktop>tsc karta.ts karta.ts:8:7 - error TS2322: Type '{ jmeno: string; popis: number; cena: number; sk ladem: true; }' is not assignable to type 'Produkt'. Types of property 'popis' are incompatible. Type 'number' is not assignable to type 'string'. 8 const plysak: Produkt = {
Dostaneme rovnakú chybu, akú by nám nahlásil editor. Všetko je teda v poriadku, aj bez chytrého editora sme chybu odhalili a môžeme si ju opraviť:
const plysak: Produkt = { jmeno: 'Plyšový medvěd', popis: 'Plyšová hračka zobrazující medvěda v sedu.', cena: 223, skladem: true }
Interface ako typ parametra funkcie
Teraz si ukážeme hlavný dôvod prečo interface
používať.
Vytvoríme si funkciu a využijeme interface
, aby nám postrážil,
čo jej odovzdávame:
function zobrazKartu(produkt: Produkt) { const element = document.getElementById('karta'); const karta = ` <h3 class="header">${produkt.jmeno}</h3> <p class="popis">${produkt.popis}</p> <p class="sklad">${produkt.skladem ? 'skladem' : 'vyprodáno'}</p> <p class="cena">cena: ${produkt.cena}</p> `; element.innerHTML = karta; }
Funkcia vytvára kartu pre zobrazenie nášho medveďa. V rýchlosti si ju
popíšeme. Naše funkcie si nájde element s id="karta"
a vytvorí
kartu z našich dát. Všimnite si, že pre vygenerovanie HTML kódu používame
viacriadkový reťazec, deklarovaný pomocou spätných úvodzoviek. Premenné
alebo výrazy do reťazca vkladáme pomocou interpolácie, teda sekvencií
${promenna nebo vyraz}
. S pomocou ternárního operátora sa
rozhoduje, či sa v položke "sklade" zobrazí "skladom" alebo "vypredané".
Nakoniec nám to funkcia všetko vypíše do <div>
u s
id="karta"
.
Ako je vidieť, v parametroch funkcia je u parametra produkt
definované, že ide o typ Produkt
. Vďaka tomu nie je možné do
funkcie vložiť iné objekty ako tie obsahujúce vlastnosti a metódy, ktoré
určuje dané rozhranie. Keď budú obsahovať niečo naviac, nebude to
vadiť.
Možno si myslíte, že nie je potrebné, aby za vás
štruktúru objektu TypeScript strážil a že ide len o zbytočný kód
navyše. Verte mi, že akonáhle váš projekt dosiahne určitej veľkosti,
budete radi, že ste si interface
napísali. Inak sa nič nemení,
všetko ostatné je starý známy JavaScript.
Nepovinná vlastnosť
V praxi sa nám môže stať, že budeme potrebovať definovať nepovinnú
vlastnosť. K tomuto účelu použijeme otáznik. Rozšírime náš produkt o
vlastnosť barva?
. Nie každý produkt musí mať túto vlastnosť,
takže ju zadáme ako nepovinnú:
interface Produkt { jmeno: string; popis: string; cena: number; skladem: boolean; barva?: string; }
Teraz ak u nášho medvedíka špecifikujeme, alebo úplne vynecháme
vlastnosť barva?
, Kompiler to stále vyhodnotí ako validný kód.
Nesmieme zabudnúť, že TypeScript za nás síce stráži náš kód, ale ak
vlastnosti barva?
nenastavíte hodnotu, bude
undefined
. Pri generovaní našej karty je teda potrebné tento
prípad ošetriť:
function zobrazKartu(produkt: Produkt): void { const element = document.getElementById('karta'); const karta = ` <h3 class="header">${produkt.jmeno}</h3> <p class="popis">${produkt.popis}</p> ${produkt.barva ? `<p class="barva">barva: ${produkt.barva}</p>` : ''} <p class="sklad">${produkt.skladem ? 'skladem' : 'vyprodáno'}</p> <p class="cena">cena: ${produkt.cena}</p> `; element.innerHTML = karta; }
Ošetrenie zadanie farby sme vykonali cez ďalšie ternárne výraz. Na kódu
si môžete všimnúť ešte jednej zaujímavej veci. Za definíciou parametrov
funkcie pribudlo : void
. TypeScript nám totiž neumožňuje len
strážiť vstupné atribúty, ale aj to, čo naše funkcia vracia. V tomto
prípade nevracia nič, zadáme teda void
. Možno vás zaujíma,
ako vyzerá JavaScript kód po kompilácii. Možno vás to trochu prekvapí.
function zobrazKartu(produkt) { var element = document.getElementById("karta"); var karta = '\n <h3 class="header">' + produkt.jmeno + '</h3>\n <p class="popis">' + produkt.popis + "</p>\n " + (produkt.barva ? '<p class="barva">barva: ' + produkt.barva + "</p>" : "") + '\n <p class="sklad">' + (produkt.skladem ? "skladem" : "vyprodáno") + '</p>\n <p class="cena">cena: ' + produkt.cena + "</p>\n "; element.innerHTML = karta; } var plysak = { jmeno: "Plyšový medvěd", popis: "Plyšová hračka zobrazující medvěda v sedu.", cena: 223, barva: "modra", skladem: true }; zobrazKartu(plysak);
Toto je celý náš kód po kompilácii. Ako vidíme, všetky
interface
a označenie typov sú preč. JavaScript ako taký nemá
žiadny ekvivalent pre túto funkcionalitu a je teda kompilátorom vynechaná.
Interface slúži pre vaše pohodlie a kontrolu pred a počas
kompilácie.
Implementácia rozhrania v triedach
Možno vás napadlo, k čomu vlastne rozhranie je, keď sme rovnako tak dobre
mohli definovať triedu Produkt
a odovzdať funkciu
zobrazKartu()
jej inštanciu. Tiež by predsa bolo skontrolované,
či je parameter naozaj inštancie triedy Produkt
a či má všetky
vlastnosti a metódy, ktoré trieda deklaruje. My však nechceme
obmedziť parameter na jednu triedu, iba chceme skontrolovať, či
parameter niečo obsahuje, ale môže byť akéhokoľvek typu.
Rozhranie nás na konkrétny typ neobmedzujú.
Urobme si ďalší príklad a implementujte si rozhrania tentokrát v triede, miesto v anonymnom objektu.
Každá trieda môže implementovať ľubovoľný počet rozhraní, čo by sa nemalo pliesť s dedičnosťou, pri ktorej môžeme dediť vždy len z jednej triedy.
Aby sme si rozhrania poriadne vyskúšali, vytvoríme si ešte druhé
rozhranie a rovno dve triedy, reprezentujúce produkt. Následne si vyskúšame,
že inštancie oboch tried môžeme funkciu zobrazKartu()
odovzdať
a to aj keď to sú iné dátové typy, avšak národné implementačné
rovnaké rozhranie.
interface Produkt { jmeno: string; popis: string; cena: number; skladem: boolean; barva?: string; } interface Hodnotitelny { celkoveHodnoceni: number; pocetHodnoticich: number; } class ProduktEshopuA implements Produkt, Hodnotitelny { jmeno: string; popis: string; cena: number; skladem: boolean; celkoveHodnoceni: number; pocetHodnoticich: number; } class ProduktEshopuB implements Produkt { jmeno: string; popis: string; cena: number; skladem: boolean; barva: string; pocetKomentaru: number; } class Clanek implements Hodnotitelny { titulek: string; text: string; celkoveHodnoceni: number; pocetHodnoticich: number; } function zobrazKartu(produkt: Produkt): void { const element = document.getElementById('karta'); const karta = ` <h3 class="header">${produkt.jmeno}</h3> <p class="popis">${produkt.popis}</p> ${produkt.barva ? `<p class="barva">barva: ${produkt.barva}</p>` : ''} <p class="sklad">${produkt.skladem ? 'skladem' : 'vyprodáno'}</p> <p class="cena">cena: ${produkt.cena}</p> `; element.innerHTML = karta; } function zobrazHodnoceni(obj: Hodnotitelny): void { const element = document.getElementById('hodnoceni'); const hodnoceni = ` <p class="hodnoceni">${obj.celkoveHodnoceni}</p> <p class="hodnotilo">hidnotilo: ${obj.pocetHodnoticich}</p> `; element.innerHTML = hodnoceni; } zobrazKartu(new ProduktEshopuA()) zobrazKartu(new ProduktEshopuB())
Produkty by bolo samozrejme ideálne najprv naplniť dátami, ale vidíme, že kód sa preložil.
Okrem rozhrania Produkt
sme ešte pridali druhé rozhranie,
Hodnotitelny
. To môžeme implementovať v triedach, ktoré možno
hodnotiť a potom týmto objektom hodnotenie jednoducho vypísať raz
univerzálny funkcií (či je to už produkt alebo napríklad článok). Vďaka
rozhraniu to bude hračka.
Čo sa týka produktov, môžete si predstaviť, že jedna trieda je produkt z eshopu A a druhá je produkt z eshopu B. Oba tieto obchody majú trochu iné produkty, ale vďaka dodržanie jednotného rozhrania si môžu medzi sebou tieto obchody vymieňať javascriptové knižnice, ktoré s ich produkty vždy korektne fungujú.
Rozhranie v triede implementujeme pomocou kľúčového slova
implements
. Ak trieda ešte z nejakej inej triedy dedí,
zvyčajne to zapíšeme ako class A extends B, implements C, D
.
A čo polia?
Na záver si ukážme ako využiť rozhranie pre prácu s poľom. Podobne ako
definujeme interface
objektov, môžeme definovať
interface
pre pole a to ako pre kľúč, tak pre hodnotu.
interface seznam { [index: number]: string; } // chyba: Type 'number' is not assignable to type 'string'. let list2: seznam = ["Honza", 2, "Petr"];
Vyššie sme si definovali rozhrania seznam
pre pole o
číselných kľúčoch a textových položkách. Následné vytvorenie
takéhoto zoznamu, avšak s jednou číselnou položkou, vyvolá pri preklade do
TypeScriptu chybu.
Skúsme si ešte posledný príklad:
interface narozeniny { [index: string]: number; } let seznamOslavencu: narozeniny; // v pořádku seznamOslavencu["Honza"] = 1505; // chyba: Type '"patnáctéhokvětna"' is not assignable to type 'number'. seznamOslavencu[2] = 'patnáctéhokvětna';
Tu je to opačne, kľúče sú textové a hodnoty číselné. Posledný riadok teda vyvolá chybu zatiaľ čo ten predchádzajúci sa vykoná.
Snáď vám použitie interface
už teraz dáva zmysel. Ide
naozaj o kontrolu či daný typ niečo spĺňa, ale nie, či je to nejaký
konkrétny typ. Určite sa s nimi budeme stretávať ešte často. V prílohe
nájdete plne funkčný kartu nášho medvedíka aj so štýlmi. Len jediné čo
nechám na vás, je kompilácia z TypeScriptu
V budúcej lekcii, Generiká, enum a tuple v TypeScriptu , sa pozrieme na generiká, enum a tuple.
Mal si s čímkoľvek problém? Stiahni si vzorovú aplikáciu nižšie a porovnaj ju so svojím projektom, chybu tak ľahko nájdeš.
Stiahnuť
Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami
Stiahnuté 107x (1.27 kB)
Aplikácia je vrátane zdrojových kódov v jazyku JavaScript