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:

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 109x (1.27 kB)
Aplikace je včetně zdrojových kódů v jazyce JavaScript