Rozhraní je tvar objektu. Standardní objekt jazyka JavaScript je mapa dvojic key:value. Klíče objektů JavaScript jsou téměř ve všech případech řetězce a jejich hodnoty jsou libovolné podporované hodnoty jazyka JavaScript (primitivní nebo abstraktní).

Rozhraní říká překladači jazyka TypeScript, jaká jména vlastností může objekt mít a jaké jsou jejich odpovídající typy hodnot. Rozhraní je tedy typ a je to abstraktní typ, protože se skládá z primitivních typů.

Když definujeme objekt s vlastnostmi (klíči) a hodnotami, TypeScript vytvoří implicitní rozhraní tím, že se podívá na názvy vlastností a datový typ jejich hodnot v objektu. Děje se tak díky typové inferenci.

(object-shape.ts)

V uvedeném příkladu jsme vytvořili objekt student s poli firstName, lastName, age a getSalary a přiřadili mu některé počáteční hodnoty. Na základě těchto informací vytvoří TypeScript implicitní typ rozhraní pro student.

{
firstName: string;
lastName: string;
age: number;
getSalary: (base: number) => number;
}

Rozhraní je stejně jako objekt, ale obsahuje pouze informace o vlastnostech objektu a jejich typech. Můžeme také vytvořit typ rozhraní a dát mu jméno, abychom ho mohli použít k anotaci hodnot objektu, ale zde toto rozhraní nemá jméno, protože bylo vytvořeno implicitně. Můžete to porovnat s typem funkce v předchozí lekci, který byl nejprve vytvořen implicitně a poté jsme vytvořili typ funkce explicitně pomocí typového aliasu.

Zkusíme si pohrát s vlastnostmi objektu poté, co byl definován.

(shape-override.ts)

Jak vidíte z výše uvedeného příkladu, TypeScript si pamatuje tvar objektu, protože typ ross je implicitní rozhraní. Pokud se pokusíme přepsat hodnotu vlastnosti hodnotou jiného typu, než jaký je uveden v rozhraní, nebo se pokusíme přidat novou vlastnost, která není uvedena v rozhraní, kompilátor jazyka TypeScript program nezkompiluje.

Pokud chcete, aby měl objekt v podstatě libovolnou vlastnost, můžete explicitně označit hodnotu any a kompilátor jazyka TypeScript nebude odvozovat typ z přiřazené hodnoty objektu. Přesně toho lze dosáhnout i jinými, lepšími způsoby, které si v tomto článku projdeme.

(shape-override-any.ts)

Deklarace rozhraní

Takže implicitní rozhraní, které jsme dosud viděli, je technicky vzato typ, ale nebylo definováno explicitně. Jak již bylo řečeno, rozhraní není nic jiného než tvar, který může objekt nabývat. Pokud máme funkci, která přijímá argument, který by měl být objektem, ale určitého tvaru, pak musíme tento argument (parametr) anotovat typem rozhraní.

(argument-with-shape.ts)

V uvedeném příkladu jsme definovali funkci getPersonInfo, která přijímá argument objektu, který má firstName, lastName, age a getSalary pole uvedených datových typů. Všimněte si, že jsme jako typ pomocí anotace :<type> použili objekt, který obsahuje názvy vlastností a jim odpovídající typy. Toto je příklad anonymního rozhraní, protože rozhraní nemá jméno, bylo použito inline.

Toto vše se zdá být poněkud komplikované na zpracování. Pokud se objekt ross zkomplikuje a je třeba ho použít na více místech, zdá se, že TypeScript je prostě věc, která se vám zpočátku líbila, ale nyní se s ní jen těžko pracuje. Tento problém vyřešíme tak, že pomocí klíčového slova interface definujeme typ rozhraní.

(argument-with-interface.ts)

V uvedeném příkladu jsme definovali rozhraní Person, které popisuje tvar objektu, ale tentokrát máme k dispozici název, kterým můžeme na tento typ odkazovat. Tento typ jsme použili k anotaci proměnné ross i argumentu person funkce getPersonIfo. To bude informovat jazyk TypeScript, aby tyto entity ověřil podle tvaru Person.

Proč používat rozhraní?

Typ rozhraní může být důležitý pro vynucení určitého tvaru. Obvykle v JavaScriptu za běhu slepě věříme, že objekt bude vždy obsahovat určitou vlastnost a že tato vlastnost bude mít vždy hodnotu určitého typu, jako je například {age: 21, ...}

Když skutečně začneme provádět operace s touto vlastností, aniž bychom nejprve zkontrolovali, zda tato vlastnost na objektu existuje nebo zda je její hodnota taková, jakou jsme očekávali, může se něco pokazit a vaše aplikace pak může zůstat nepoužitelná. Například {age: '21', ...}, zde age hodnota je string.

Rozhraní poskytují bezpečný mechanismus pro řešení takových scénářů v době kompilace. Pokud omylem použijete vlastnost u objektu, který neexistuje, nebo použijete hodnotu vlastnosti v nepovolené operaci, kompilátor jazyka TypeScript váš program nezkompiluje. Podívejme se na příklad:

(interface-safety.ts)

Ve výše uvedeném příkladu se snažíme použít vlastnost name argumentu _student uvnitř funkce printStudent. Protože argument _student je typem rozhraní Student, kompilátor jazyka TypeScript při kompilaci vyhodí chybu, protože tato vlastnost v rozhraní Student neexistuje.

Podobně 100 — _student.firstName není platnou operací, protože vlastnost firstName je typem rozhraní string a pokud vím, v JavaScriptu nelze odečíst string od number (výsledkem je NaN).

V uvedeném příkladu jsme použili tradiční způsob zápisu typu funkce pro pole getSalary. Pro totéž však můžete použít také syntaxi funkce bez těla, která se obecně používá v rozhraních.

interface Student {
firstName: string;
lastName: string;
age: number;
getSalary(base: number): number;
};

Volitelné vlastnosti

Někdy potřebujete, aby objekt měl vlastnost, která uchovává data určitého datového typu, ale není povinné, aby objekt tuto vlastnost měl. Je to podobné jako u nepovinných parametrů funkcí, které jsme se naučili v předchozí lekci.

Takovým vlastnostem se říká nepovinné vlastnosti. Rozhraní může obsahovat volitelné vlastnosti a k jejich reprezentaci používáme anotaci ?:Type, stejně jako u volitelných parametrů funkcí.

(optional-properties.ts)

V uvedeném příkladu má rozhraní Student vlastnost age, která je volitelná. Pokud je však vlastnost age uvedena, musí mít hodnotu typu number.

V případě objektu ross, který je typem rozhraní Student, jsme neuvedli hodnotu vlastnosti age, která je legální, avšak v případě objektu monica jsme uvedli vlastnost age, ale její hodnota je string, což není legální. Proto překladač jazyka TypeScript vyhodí chybu.

Chybička se může zdát podivná, ale ve skutečnosti dává smysl. Pokud vlastnost age u objektu neexistuje, object.age vrátí undefined, což je typ undefined. Pokud existuje, pak musí být hodnota typu number.

Z toho vyplývá, že hodnota vlastnosti age může být buď typu undefined, nebo number, což se v jazyce TypeScript reprezentuje pomocí syntaxe union number | undefined.

💡 O typech unions se budeme učit v lekci Type System.

Nepovinné vlastnosti však při provádění programu představují vážný problém. Představme si, že při aritmetické operaci používáme vlastnost age, ale její hodnota je undefined. To je svého druhu vážný problém.

Dobré ale je, že překladač jazyka TypeScript neumožňuje provádět nelegální operace s nepovinnou vlastností, protože její hodnota může být undefined.

(optional-properties-safety.ts)

V uvedeném příkladu provádíme aritmetickou operaci nad vlastností age, což je nelegální, protože hodnota této vlastnosti může být za běhu number nebo undefined. Výsledkem provádění aritmetických operací nad undefined je NaN (není to číslo).

💡 Pro výše uvedený program jsme však museli tp nastavit příznak --strictNullChecks na false, což je příznak kompilátoru jazyka TypeScript. Pokud tuto možnost zajistíme, výše uvedený program se zkompiluje v pořádku.

Abychom se této chybě nebo varování vyhnuli, musíme kompilátoru jazyka TypeScript výslovně sdělit, že tato vlastnost je typu number a nikoliv number nebo undefined. K tomu použijeme typ assertion (AKA type conversion nebo typecasting).

(optional-properties-safety-override.ts)

V uvedeném programu jsme použili (_student.age as number), který převede typ _student.age z number | undefined na number. Tímto způsobem sdělíme kompilátoru jazyka TypeScript: „Hej, tohle je číslo“. Lepší způsob, jak to řešit, by však bylo také za běhu zkontrolovat, zda _student.age je undefined, a pak provést aritmetickou operaci.

💡 O typových tvrzeních se dozvíme v lekci Type System.

Typ funkce pomocí rozhraní

Není to jen tvar prostého objektu, ale rozhraní může také popisovat signaturu funkce. V předchozí lekci jsme k popisu typu funkce použili typový alias, ale rozhraní to umí také.

interface InterfaceName {
(param: Type): Type;
}

Syntaxe pro deklaraci rozhraní jako typu funkce je podobná jako u samotné signatury funkce. Jak vidíte z výše uvedeného příkladu, tělo rozhraní obsahuje přesnou signaturu anonymní funkce, samozřejmě bez těla. Na jménech parametrů zde nezáleží.

(signatura funkce.ts)

V příkladu výše jsme definovali rozhraní IsSumOdd, které definuje typ funkce, která přijímá dva argumenty typu number a vrací hodnotu boolean. Nyní můžete tento typ použít k popisu funkce, protože typ rozhraní IsSumOdd je ekvivalentní typu funkce (x: number, y: number) => boolean.

Rozhraní se signaturou anonymní metody popisuje funkci. Funkce v oblasti JavaScriptu je však také objekt, což znamená, že k hodnotě funkce můžete přidávat vlastnosti stejně jako k objektu. Proto je zcela legální, že na rozhraní typu funkce můžete definovat libovolné vlastnosti.

(function-signature-with-methods.ts)

V příkladu výše jsme přidali type a calculate vlastnosti na rozhraní IsSumOdd, které popisuje funkci. Pomocí metody Object.assign sloučíme vlastnosti type a calculate s hodnotou funkce.

Rozhraní typu funkce může být užitečné pro popis funkce konstruktoru. Funkce konstruktoru je podobná třídě, jejímž úkolem je vytvářet objekty (instance). Až do jazyka ES5 jsme měli k dispozici pouze funkce konstruktoru, které napodobovaly classv jazyce JavaScript. Proto TypeScript kompiluje třídy do konstrukčních funkcí, pokud se zaměřujete na ES5 nebo nižší.

💡 Pokud se chcete o konstrukčních funkcích dozvědět více, sledujte tento článek.

Pokud před signaturu anonymní funkce v rozhraní vložíme klíčové slovo new, učiní funkci konstruovatelnou. To znamená, že funkci lze vyvolat pouze pomocí klíčového slova new pro generování objektů, nikoli pomocí běžného volání funkce. Ukázka konstrukční funkce vypadá následovně:

function Animal( _name ) {
this.name = _name;
}var dog = new Animal( 'Tommy' );
console.log( dog.name ); // Tommy

Naštěstí nemusíme pracovat s konstrukčními funkcemi, protože TypeScript poskytuje klíčové slovo class pro vytvoření třídy, se kterou se pracuje mnohem snadněji než s konstrukční funkcí, to mi věřte. Ve skutečnosti je class hluboko uvnitř konstrukční funkce v jazyce JavaScript. Zkuste si níže uvedený příklad:

class Animal{
constructor( _name ) {
this.name = _name;
}
}console.log( typeof Animal ); // "function"

Třída a funkce konstruktoru jsou jedna a tatáž věc. Jediný rozdíl je v tom, že class nám poskytuje bohatou OOP syntaxi, se kterou můžeme pracovat. Proto rozhraní typu funkce konstruktoru představuje třídu.

(funkce-signature-with-new.ts)

V uvedeném příkladu jsme definovali třídu Animal s funkcí konstruktoru, která přijímá argument typu string. Můžete to považovat za funkci konstruktoru, která má podobnou signaturu jako konstruktor Animal.

Třída AnimalInterface definuje funkci konstruktoru, protože má anonymní funkci s předponou new. To znamená, že třída Animal splňuje podmínky pro to, aby byla typem třídy AnimalInterface. Zde je typ rozhraní AnimalInterface ekvivalentní typu funkce new (sound: string) => any.

Funkce createAnimal přijímá ctor argument typu AnimalInterface, proto můžeme jako hodnotu argumentu předat třídu Animal. Signaturu metody getSound třídy Animal v AnimalInterface nebudeme moci přidat a důvod je vysvětlen v lekci Třídy.

Indexovatelné typy

Indexovatelný objekt je objekt, k jehož vlastnostem lze přistupovat pomocí indexové signatury jako obj. To je standardní způsob přístupu k prvku pole, ale můžeme to udělat i pro objekt.

var a = ;
var o = { one: 1, two: 2, three: 3 };console.log( a ); // 1
console.log( a ); // 1 (same as `a.one`)

Objekt může mít někdy libovolný počet vlastností bez určitého tvaru. V takovém případě stačí použít typ object. Tento typ object však definuje libovolnou hodnotu, která není number, string, boolean, symbol, null nebo undefined, jak bylo řečeno v lekci o základních typech.

Potřebujeme-li striktně kontrolovat, zda je hodnota obyčejným objektem JavaScriptu, pak můžeme mít problém. To lze vyřešit pomocí typu rozhraní se signaturou indexu pro název vlastnosti.

interface SimpleObject {
: any;
}

Rozhraní SimpleObject definuje tvar objektu s string klíči, jehož hodnoty mohou být datového typu any. Zde je key název vlastnosti použit pouze jako zástupný znak, protože je uzavřen v hranatých závorkách.

(indexable-type-object.ts)

V uvedeném příkladu jsme definovali ross a monica objekt typu SimpleObject rozhraní. Protože tyto objekty obsahují klíče string a hodnoty datového typu any, je to zcela legální.

Pokud vás mate klíč 1 v objektu monica, který je typu number, je to legální, protože položky objektů nebo polí v JavaScriptu lze indexovat pomocí klíčů number nebo string, jak je uvedeno níže.

var o = { 0: 'Zero', '1': 'One' };
var a = ;console.log( o ); // Zero
console.log( o ); // One
console.log( a ); // One
console.log( a ); // One

Pokud potřebujeme upřesnit typ klíčů a jejich hodnot, můžeme to jistě také udělat. Chceme-li, můžeme například definovat typ indexovatelného rozhraní s klíči typu number a hodnotami typu number.

💡 Typ klíče indexového podpisu musí být buď string, nebo number.

(indexable-type-number.ts)

V uvedeném příkladu jsme definovali rozhraní LapTimes, které může obsahovat názvy vlastností typu number a hodnoty typu number. Toto rozhraní může reprezentovat datovou strukturu, kterou lze indexovat pomocí klíčů number, proto jsou pole ross a objekty monica a joey legální.

Objekt rachel však nevyhovuje tvaru LapTimes, protože klíč one je string a lze k němu přistupovat pouze pomocí string, jako je rachel, a nic jiného. Proto kompilátor jazyka TypeScript vyhodí chybu, jak je uvedeno výše.

V indexovatelném typu rozhraní je možné mít některé vlastnosti povinné a některé nepovinné. To může být docela užitečné, když potřebujeme, aby objekt měl určitý tvar, ale je opravdu jedno, jestli v objektu dostaneme další a nechtěné vlastnosti.

(indexable-type-required-properties.ts)

V uvedeném příkladu jsme definovali rozhraní LapTimes, které musí obsahovat vlastnost name s hodnotou string a nepovinnou vlastnost age s hodnotou number. Objekt typu LapTimes může mít také libovolné vlastnosti, jejichž klíče musí být number a jejichž hodnoty by měly být také number.

Objekt ross je platný objekt LapTimes, i když nemá vlastnost age, protože je nepovinná. Objekt monica však má vlastnost age, ale jeho hodnota je string, tudíž nevyhovuje rozhraní LapTimes.

Objekt joey také nevyhovuje rozhraní LapTimes, protože má vlastnost gender, která je typu string. Objekt rachel nemá vlastnost name, která je v rozhraní LapTimes vyžadována.

💡 Při používání indexovatelných typů musíme dávat pozor na některé háčky. Ty jsou zmíněny v této dokumentaci.

Rozšíření rozhraní

Stejně jako třídy může rozhraní dědit vlastnosti z jiných rozhraní. Na rozdíl od tříd v jazyce JavaScript však rozhraní může dědit z více rozhraní. Pro dědění rozhraní používáme klíčové slovo extends.

(extend-interface.ts)

Rozšířením rozhraní získá potomek rozhraní všechny vlastnosti rodičovského rozhraní. To je docela užitečné, když má více rozhraní společnou strukturu a chceme se vyhnout duplikaci kódu tím, že společné vlastnosti vyvedeme do společného rozhraní, které lze později zdědit.

(extend-multiple-interfaces.ts)

V uvedeném příkladu jsme vytvořili rozhraní Student, které dědí vlastnosti z rozhraní Person a Player.

Deklarace vícenásobných rozhraní

V předchozí části jsme se naučili, jak může rozhraní dědit vlastnosti jiného rozhraní. To jsme provedli pomocí klíčového slova extend. Pokud jsou však rozhraní se stejným názvem deklarována ve stejném modulu (souboru), TypeScript sloučí jejich vlastnosti dohromady, pokud mají odlišné názvy vlastností nebo jsou jejich konfliktní typy vlastností stejné.

(merged-interface.ts)

V uvedeném příkladu jsme deklarovali rozhraní Person několikrát. Sloučením vlastností všech deklarací rozhraní Person vznikne jediná deklarace rozhraní Person.

💡 V lekci Třídy jsme se naučili, že class implicitně deklaruje rozhraní a rozhraní může toto rozhraní rozšiřovat. Pokud tedy program obsahuje třídu Person a rozhraní Person, pak výsledný typ Person (rozhraní) bude mít sloučené vlastnosti mezi třídou a rozhraním.

Vložená rozhraní

Rozhraní může mít hluboce vnořené struktury. V následujícím příkladu definuje pole info rozhraní Student tvar objektu s vlastnostmi firstName a lastName.

(nested-shape.ts)

Podobně je zcela legální, aby pole rozhraní mělo typ jiného rozhraní. V následujícím příkladu má pole info rozhraní Student typ rozhraní Person.

(nested-interface.ts)

.

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna.