IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Lekce 4 - Interface (rozhraní) v TypeScriptu

V předešlém cvičení, Řešené úlohy k 3. lekci v TypeScriptu, jsme si procvičili nabyté zkušenosti z předchozích lekcí.

Dnes se podíváme na jednu z nejpraktičtějších, a pokud pracujete s větším množstvím dat, tak i nejproblematič­tějších konstrukcí, které TypeScript do JavaScriptu přináší - interface. Pokud jste nikdy nezavadili o typové programovací jazyky, bude pro vás toto téma ze začátku možná trochu matoucí. Nebojte, slibuji, že s praxí se to zlepší.

Co interface dělá?

Interface neboli rozhraní je jakási smlouva programátora s programem, že proměnná bude obsahovat data nebo metody v požadovaném tvaru. Pokud je váš kód používán ve větším týmu nebo ho používá někdo úplně jiný, pomáhají mu rozhraní porozumět, co vaše metody vyžadují na vstupu. Pokud nedodržíte správný tvar objektu, váš editor, pokud touto funkcí disponuje, vám napoví co je s vaším kódem špatně. Tím se dá docílit bezpečné komunikace mezi několika objekty v programu.

Chápu, že do začátku vám to moc neříká. To je naprosto v pořádku. Pojďme se podívat na příklad.

Založte si soubor karta.ts a do něj napište:

interface Produkt {
    jmeno: string;
    popis: string;
    cena: number;
    skladem: boolean;
}

Toto je interface produktu, který může obsahovat např. nějaký e-shop. Jak je vidět, objekt produkt bude vždy obsahovat vlastnosti jmeno, popis, cena a informaci zda je skladem. Žádnou z těchto vlastností není možné vynechat. Zároveň nám interface stanoví, jaký typ vlastnosti mají. Nyní ho zkusíme použít.

Implementace interface

Chceme-li implementovat interface v nějaké proměnné, provedeme to přidáním dvojtečky za název proměnné, jak je vidět na příkladu:

const plysak: Produkt = {
    jmeno: 'Plyšový medvěd',
    popis: 5,
    cena: 223,
    skladem: true
}

Kód výše vypíše chybu. Níže je ukázka z mého Visual Studia Code:

Chyba při implementaci rozhraní v TypeScriptu ve Visual Studio Code - TypeScript

Jak je vidět, dostal jsem vynadáno, protože vlastnost popis není kompatibilní s typem number. A aby mi to program ještě více usnadnil, tak mi napoví, jak má celý objekt vypadat. Pokud nemáte editor, který by byl schopen hlídat kód za chodu, zkuste aplikaci zkompilovat. Kompilujeme příkazem v příkazové řádce:

tsc karta.ts

Výsledek:

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 stejnou chybu, jakou by nám nahlásil editor. Vše je tedy v pořádku, i bez chytrého editoru jsme chybu odhalili a můžeme si jí opravit:

const plysak: Produkt = {
    jmeno: 'Plyšový medvěd',
    popis: 'Plyšová hračka zobrazující medvěda v sedu.',
    cena: 223,
    skladem: true
}

Interface jako typ parametru funkce

Nyní si ukážeme hlavní důvod proč interface používat. Vytvoříme si funkci a využijeme interface, aby nám pohlídal, co jí předáváme:

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

Funkce vytváří kartu pro zobrazení našeho medvěda. V rychlosti si ji popíšeme. Naše funkce si najde element s id="karta" a vytvoří kartu z našich dat. Všimněte si, že pro vygenerování HTML kódu používáme víceřádkový řetězec, deklarovaný pomocí zpětných uvozovek. Proměnné nebo výrazy do řetězce vkládáme pomocí interpolace, tedy sekvencí ${promenna nebo vyraz}. S pomocí ternárního operátoru se rozhoduje, zda se v položce "skladem" zobrazí "skladem" nebo "vyprodáno". Nakonec nám to funkce vše vypíše do <div>u s id="karta".

Jak je vidět, v parametrech funkce je u parametru produkt definováno, že jde o typ Produkt. Díky tomu není možné do funkce vložit jiné objekty než ty obsahující vlastnosti a metody, které určuje dané rozhraní. Když budou obsahovat něco navíc, nebude to vadit.

Možná si myslíte, že není třeba, aby za vás strukturu objektu TypeScript hlídal a že jde jen o zbytečný kód navíc. Věřte mi, že jakmile váš projekt dosáhne určité velikosti, budete rádi, že jste si interface napsali. Jinak se nic nemění, vše ostatní je starý známý JavaScript.

Nepovinná vlastnost

V praxi se nám může stát, že budeme potřebovat definovat nepovinnou vlastnost. K tomuto účelu použijeme otazník. Rozšíříme náš produkt o vlastnost barva?. Ne každý produkt musí mít tuto vlastnost, takže ji zadáme jako nepovinnou:

interface Produkt {
    jmeno: string;
    popis: string;
    cena: number;
    skladem: boolean;
    barva?: string;
}

Nyní pokud u našeho medvídka specifikujeme, nebo úplně vynecháme vlastnost barva?, kompiler to stále vyhodnotí jako validní kód. Nesmíme zapomenout, že TypeScript za nás sice hlídá náš kód, ale pokud vlastnosti barva? nenastavíme hodnotu, bude undefined. Při generování naší karty je tedy třeba tento případ ošetřit:

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šetření zadání barvy jsme provedli přes další ternární výraz. Na kódu si můžete všimnout ještě jedné zajímavé věci. Za definicí parametrů funkce přibylo : void. TypeScript nám totiž neumožňuje pouze hlídat vstupní atributy, ale i to, co naše funkce vrací. V tomto případě nevrací nic, zadáme tedy void. Možná vás zajímá, jak vypadá JavaScript kód po kompilaci. Možná vás to trochu překvapí.

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 kompilaci. Jak vidíme, všechny interface a označení typů jsou pryč. JavaScript jako takový nemá žádný ekvivalent pro tuto funkcionalitu a je tedy kompilátorem vynechána. Interface slouží pro vaše pohodlí a kontrolu před a během kompilace.

Implementace rozhraní ve třídách

Možná vás napadlo, k čemu vlastně rozhraní je, když jsme stejně tak dobře mohli definovat třídu Produkt a předat funkci zobrazKartu() její instanci. Také by přeci bylo zkontrolováno, zda je parametr opravdu instance třídy Produkt a zda má všechny vlastnosti a metody, které třída deklaruje. My ovšem nechceme omezit parametr na jednu třídu, pouze chceme zkontrolovat, zda parametr něco obsahuje, ale může být jakéhokoli typu. Rozhraní nás na konkrétní typ neomezují.

Udělejme si další příklad a implementujme si rozhraní tentokrát ve třídě, místo v anonymním objektu.

Každá třída může implementovat libovolný počet rozhraní, což by se nemělo plést s dědičností, při které můžeme dědit vždy jen z jedné třídy.

Abychom si rozhraní pořádně vyzkoušeli, vytvoříme si ještě druhé rozhraní a rovnou dvě třídy, reprezentující produkt. Následně si vyzkoušíme, že instance obou tříd můžeme funkci zobrazKartu() předat a to i když to jsou jiné datové typy, ovšem implementující stejné rozhraní.

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">hodnotilo: ${obj.pocetHodnoticich}</p>
    `;

    element.innerHTML = hodnoceni;
}

zobrazKartu(new ProduktEshopuA())
zobrazKartu(new ProduktEshopuB())

Produkty by bylo samozřejmě ideální nejprve naplnit daty, ale vidíme, že kód se přeložil.

Kromě rozhraní Produkt jsme ještě přidali druhé rozhraní, Hodnotitelny. To můžeme implementovat ve třídách, které lze hodnotit a pak těmto objektům hodnocení jednoduše vypsat jednou univerzální funkcí (ať je to již produkt nebo třeba článek). Díky rozhraní to bude hračka.

Co se týká produktů, můžete si představit, že jedna třída je produkt z eshopu A a druhá je produkt z e-shopu B. Oba tyto obchody mají trochu jiné produkty, ale díky dodržení jednotného rozhraní si mohou mezi sebou tyto obchody vyměňovat javascriptové knihovny, které s jejich produkty vždy korektně fungují.

Rozhraní ve třídě implementujeme pomocí klíčového slova implements. Pokud třída ještě z nějaké jiné třídy dědí, obvykle to zapíšeme jako class A extends B, implements C, D.

A co pole?

Na závěr si ukažme jak využít rozhraní pro práci s polem. Podobně jako definujeme interface objektů, můžeme definovat interface pro pole a to jak pro klíč, tak pro hodnotu.

interface seznam {
    [index: number]: string;
}

// chyba: Type 'number' is not assignable to type 'string'.
let list2: seznam = ["Honza", 2, "Petr"];

Výše jsme si definovali rozhraní seznam pro pole o číselných klíčích a textových položkách. Následné vytvoření takového seznamu, avšak s jednou číselnou položkou, vyvolá při překladu do TypeScriptu chybu.

Zkusme si ještě poslední pří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';

Zde je to opačně, klíče jsou textové a hodnoty číselné. Poslední řádek tedy vyvolá chybu zatímco ten předchozí se provede.

Snad vám použití interface již nyní dává smysl. Jedná se opravdu o kontrolu zda daný typ něco splňuje, ale ne, zda je to nějaký konkrétní typ. Určitě se s nimi budeme setkávat ještě často. V příloze najdete plně funkční kartu našeho medvídka i se styly. Jen jediné co nechám na vás, je kompilace z TypeScriptu :-)

V následujícím cvičení, Řešené úlohy k 4. lekci v TypeScriptu, si procvičíme nabyté zkušenosti z předchozích lekcí.


 

Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.

Stáhnout

Stažením následujícího souboru souhlasíš s licenčními podmínkami

Staženo 98x (1.27 kB)
Aplikace je včetně zdrojových kódů v jazyce JavaScript

 

Předchozí článek
Řešené úlohy k 3. lekci v TypeScriptu
Všechny články v sekci
TypeScript
Přeskočit článek
(nedoporučujeme)
Řešené úlohy k 4. lekci v TypeScriptu
Článek pro vás napsal Jiří Kvapil
Avatar
Uživatelské hodnocení:
50 hlasů
Autor se věnuje profesionálně front-endu a jezdí na všem co má kola.
Aktivity