IT rekvalifikácia. Seniorní programátori zarábajú až 6 000 €/mesiac a rekvalifikácia je prvým krokom. Zisti, ako na to!

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 najproblematic­kejší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:

Chyba pri implementácii rozhrania v TypeScriptu vo Visual Studio Code - TypeScript

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

 

Predchádzajúci článok
Riešené úlohy k 1.-2. lekciu v TypeScriptu
Všetky články v sekcii
TypeScript
Preskočiť článok
(neodporúčame)
Generiká, enum a tuple v TypeScriptu
Článok pre vás napísal Jiří Kvapil
Avatar
Užívateľské hodnotenie:
2 hlasov
Autor se věnuje profesionálně front-endu a jezdí na všem co má kola.
Aktivity