Interfejs jest kształtem obiektu. Standardowy obiekt JavaScript jest mapą par key:value. Kluczami obiektów JavaScript w prawie wszystkich przypadkach są łańcuchy, a ich wartościami są dowolne obsługiwane wartości JavaScript (prymitywne lub abstrakcyjne).

Interfejs mówi kompilatorowi TypeScript o nazwach właściwości, które obiekt może posiadać i odpowiadających im typach wartości. Dlatego interfejs jest typem i jest typem abstrakcyjnym, ponieważ składa się z typów prymitywnych.

Gdy definiujemy obiekt z właściwościami (kluczami) i wartościami, TypeScript tworzy niejawny interfejs, patrząc na nazwy właściwości i typ danych ich wartości w obiekcie. Dzieje się tak dzięki wnioskowaniu typu.

(object-shape.ts)

W powyższym przykładzie utworzyliśmy obiekt student z polami firstName, lastName, age i getSalary i przypisaliśmy im pewne wartości początkowe. Używając tych informacji, TypeScript tworzy niejawny typ interfejsu dla student.

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

Interfejs jest taki sam jak obiekt, ale zawiera tylko informacje o właściwościach obiektu i ich typach. Możemy również utworzyć typ interfejsu i nadać mu nazwę, abyśmy mogli używać go do adnotowania wartości obiektu, ale tutaj ten interfejs nie ma nazwy, ponieważ został utworzony niejawnie. Możesz porównać to z typem funkcji w poprzedniej lekcji, który został utworzony niejawnie na początku, a następnie utworzyliśmy typ funkcji jawnie używając aliasu typu.

Popróbujmy pomieszać z właściwościami obiektu po jego zdefiniowaniu.

(shape-override.ts)

Jak widać na powyższym przykładzie, TypeScript zapamiętuje kształt obiektu, ponieważ typ ross jest interfejsem implicite. Jeżeli spróbujemy nadpisać wartość właściwości wartością innego typu niż określony w interfejsie lub spróbujemy dodać nową właściwość, która nie jest określona w interfejsie, kompilator TypeScript nie skompiluje programu.

Jeżeli chcesz, aby obiekt miał w zasadzie dowolną właściwość, to możesz jawnie oznaczyć wartość any i kompilator TypeScript nie będzie wnioskował o typie z przypisanej wartości obiektu. Istnieją inne, lepsze sposoby na osiągnięcie dokładnie tego celu i przejdziemy przez nie w tym artykule.

(shape-override-any.ts)

Deklaracja interfejsu

Choć domyślny interfejs, który widzieliśmy do tej pory, jest technicznie typem, ale nie został zdefiniowany jawnie. Jak już wspomnieliśmy, interfejs to nic innego jak kształt, jaki może przybrać obiekt. Jeśli mamy funkcję, która przyjmuje argument, który powinien być obiektem, ale o określonym kształcie, to musimy ten argument (parametr) opatrzyć adnotacją typu interfejsu.

(argument-with-shape.ts)

W powyższym przykładzie zdefiniowaliśmy funkcję getPersonInfo, która przyjmuje argument obiekt, który posiada pola firstName, lastName, age i getSalary o określonych typach danych. Zauważ, że użyliśmy obiektu, który zawiera nazwy właściwości i odpowiadające im typy jako typ za pomocą adnotacji :<type>. Jest to przykład anonimowego interfejsu, ponieważ interfejs nie ma nazwy, został użyty inline.

To wszystko wydaje się nieco skomplikowane w obsłudze. Jeśli obiekt ross staje się bardziej skomplikowany i musi być używany w wielu miejscach, TypeScript wydaje się rzeczą, którą początkowo lubiłeś, ale teraz jest po prostu trudna do opanowania. Aby rozwiązać ten problem, definiujemy typ interfejsu za pomocą słowa kluczowego interface.

(argument-with-interface.ts)

W powyższym przykładzie zdefiniowaliśmy interfejs Person, który opisuje kształt obiektu, ale tym razem mamy nazwę, której możemy użyć, aby odnieść się do tego typu. Użyliśmy tego typu do adnotacji ross zmiennej, jak również person argumentu funkcji getPersonIfo. To poinformuje TypeScript do walidacji tych encji względem kształtu Person.

Dlaczego warto używać interfejsu?

Typ interfejsu może być ważny, aby wymusić określony kształt. Zazwyczaj w JavaScript, pokładamy ślepą wiarę w runtime, że obiekt zawsze będzie zawierał określoną właściwość, a ta właściwość zawsze będzie miała wartość określonego typu, takiego jak {age: 21, ...} jako przykład.

Gdy faktycznie zaczynamy wykonywać operacje na tej właściwości bez uprzedniego sprawdzenia, czy ta właściwość istnieje na obiekcie lub czy jej wartość jest tym, czego oczekiwaliśmy, rzeczy mogą pójść źle i może to pozostawić twoją aplikację bezużyteczną po tym. Na przykład, {age: '21', ...}, tutaj age wartość jest string.

Interfejsy zapewniają bezpieczny mechanizm radzenia sobie z takimi scenariuszami w czasie kompilacji. Jeśli przypadkowo używasz właściwości na obiekcie, który nie istnieje lub używasz wartości właściwości w nielegalnej operacji, kompilator TypeScript nie skompiluje twojego programu. Zobaczmy przykład.

(interface-safety.ts)

W powyższym przykładzie próbujemy użyć właściwości name argumentu _student wewnątrz funkcji printStudent. Ponieważ argument _student jest typem interfejsu Student, kompilator TypeScript wyrzuca błąd podczas kompilacji, ponieważ ta właściwość nie istnieje w interfejsie Student.

Podobnie, 100 — _student.firstName nie jest poprawną operacją, ponieważ właściwość firstName jest typem string, a ostatnim razem, gdy sprawdzałem, nie można odjąć string od number w JavaScript (skutkuje NaN).

W powyższym przykładzie użyliśmy tradycyjnego sposobu pisania typu funkcji dla pola getSalary. Jednakże, można również użyć składni funkcji bez ciała dla tego samego, co jest ogólnie używane w interfejsach.

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

Właściwości opcjonalne

Czasami, potrzebujesz, aby obiekt posiadał właściwość, która przechowuje dane określonego typu danych, ale nie jest obowiązkowe posiadanie tej właściwości na obiekcie. Jest to podobne do opcjonalnych parametrów funkcji, które poznaliśmy w poprzedniej lekcji.

Takie właściwości są nazywane właściwościami opcjonalnymi. Interfejs może zawierać opcjonalne właściwości i używamy adnotacji ?:Type do ich reprezentowania, tak jak opcjonalne parametry funkcji.

(optional-properties.ts)

W powyższym przykładzie interfejs Student posiada właściwość age, która jest opcjonalna. Jeśli jednak właściwość age jest podana, to musi mieć wartość typu number.

W przypadku obiektu ross, który jest typem interfejsu Student, nie podaliśmy wartości dla właściwości age, która jest legalna, natomiast w przypadku monica, podaliśmy właściwość age, ale jej wartość to string, która nie jest legalna. Dlatego kompilator TypeScript wyrzuca błąd.

Błąd może wydawać się dziwny, ale w rzeczywistości ma sens. Jeśli właściwość age nie istnieje na obiekcie, to object.age zwróci undefined, który jest typem undefined. Jeśli istnieje, to wartość musi być typu number.

Więc wartość właściwości age może być albo typu undefined, albo number, który w TypeScripcie jest reprezentowany za pomocą składni union number | undefined.

💡 Związki typów poznamy w lekcji Type System.

Jednakże właściwości opcjonalne stwarzają poważne problemy podczas wykonywania programu. Wyobraźmy sobie, że używamy właściwości age w operacji arytmetycznej, ale jej wartością jest undefined. Jest to pewnego rodzaju poważny problem.

Ale dobrą rzeczą jest to, że kompilator TypeScript nie pozwala na wykonywanie nielegalnych operacji na właściwości opcjonalnej, ponieważ jej wartość może być undefined.

(optional-properties-safety.ts)

W powyższym przykładzie wykonujemy operację arytmetyczną na właściwości age, która jest nielegalna, ponieważ wartość tej właściwości może być number lub undefined w runtime. Wykonując operacje arytmetyczne na undefined otrzymujemy NaN (nie jest to liczba).

💡 Jednakże, dla powyższego programu musieliśmy ustawić flagę --strictNullChecks na false, która jest flagą kompilatora TypeScript. Jeżeli zapewnimy tę opcję, powyższy program skompiluje się dobrze.

Aby uniknąć tego błędu lub ostrzeżenia, musimy jawnie powiedzieć kompilatorowi TypeScript, że ta właściwość jest typu number a nie number lub undefined. W tym celu używamy asercji typu (AKA konwersja typu lub typecasting).

(optional-properties-safety-override.ts)

W powyższym programie, użyliśmy (_student.age as number), który konwertuje typ _student.age z number | undefined na number. Jest to sposób, aby powiedzieć kompilatorowi TypeScript, „Hej, to jest liczba”. Ale lepszym sposobem na poradzenie sobie z tym byłoby również sprawdzenie, czy _student.age jest undefined w runtime, a następnie wykonanie operacji arytmetycznej.

💡 O asercjach typu dowiemy się w lekcji Type System.

Typ funkcji przy użyciu interfejsu

Nie tylko kształt zwykłego obiektu, ale interfejs może również opisywać sygnaturę funkcji. W poprzedniej lekcji użyliśmy aliasu typu do opisania typu funkcji, ale interfejsy również mogą to zrobić.

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

Składnia do zadeklarowania interfejsu jako typu funkcji jest podobna do samej sygnatury funkcji. Jak widać na powyższym przykładzie, ciało interfejsu zawiera dokładną sygnaturę anonimowej funkcji, oczywiście bez ciała. Tutaj nazwy parametrów nie mają znaczenia.

(function-signature.ts)

W powyższym przykładzie zdefiniowaliśmy IsSumOddinterfejs, który definiuje typ funkcji przyjmujący dwa argumenty typu number i zwracający wartość boolean. Teraz możesz użyć tego typu do opisania funkcji, ponieważ typ interfejsu IsSumOdd jest równoważny typowi funkcji (x: number, y: number) => boolean.

Interfejs z anonimową sygnaturą metody opisuje funkcję. Ale funkcja w królestwie JavaScript jest również obiektem, co oznacza, że możesz dodać właściwości do wartości funkcji tak samo jak do obiektu. Dlatego jest całkowicie legalne, że możesz zdefiniować dowolne właściwości na interfejsie typu function.

(function-signature-with-methods.ts)

W powyższym przykładzie dodaliśmy type i calculate właściwości na interfejsie IsSumOdd, który opisuje funkcję. Używając metody Object.assign, łączymy właściwości type i calculate z wartością funkcji.

Interfejsy typu function mogą być pomocne do opisywania funkcji konstruktora. Funkcja konstruktora jest podobna do klasy, której zadaniem jest tworzenie obiektów (instancji). Do czasu ES5 mieliśmy tylko funkcje konstruktora, aby naśladować class w JavaScript. Dlatego TypeScript kompiluje klasy do funkcji konstruktora, jeśli celujesz w ES5 lub niżej.

💡 Jeśli chcesz dowiedzieć się więcej o funkcji konstruktora, prześledź ten artykuł.

Jeśli umieścimy słowo kluczowe new przed podpisem funkcji anonimowej w interfejsie, czyni to funkcję konstruowalną. Oznacza to, że funkcja może być wywołana tylko przy użyciu słowa kluczowego new do generowania obiektów, a nie przy użyciu zwykłego wywołania funkcji. Przykładowa funkcja konstruktora wygląda jak poniżej.

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

Na szczęście nie musimy pracować z funkcjami konstruktora, ponieważ TypeScript udostępnia słowo kluczowe class do tworzenia klasy, która jest znacznie łatwiejsza do pracy niż funkcja konstruktora, zaufaj mi. W rzeczywistości, class w głębi duszy jest funkcją konstruktora w JavaScript. Wypróbuj poniższy przykład.

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

Klasa i funkcja konstruktora to jedna i ta sama rzecz. Jedyną różnicą jest to, że class daje nam bogatą składnię OOP, z którą możemy pracować. Stąd interfejs typu funkcji konstruktora reprezentuje klasę.

(function-signature-with-new.ts)

W powyższym przykładzie zdefiniowaliśmy klasę Animal z funkcją konstruktora, która akceptuje argument typu string. Można to uznać za funkcję konstruktora, która ma podobną sygnaturę jak konstruktor Animal.

Klasa AnimalInterface definiuje funkcję konstruktora, ponieważ ma funkcję anonimową poprzedzoną słowem kluczowym new. Oznacza to, że klasa Animal kwalifikuje się do bycia typem AnimalInterface. Tutaj typ interfejsu AnimalInterface jest równoważny typowi funkcji new (sound: string) => any.

Funkcja createAnimal przyjmuje ctor argument typu AnimalInterface, stąd możemy przekazać klasę Animal jako wartość argumentu. Nie będziemy mogli dodać getSound sygnatury metody klasy Animal w AnimalInterface, a powód jest wyjaśniony w lekcji Classes.

Typy indeksowalne

Obiekt indeksowalny to obiekt, do którego właściwości można uzyskać dostęp za pomocą sygnatury indeksu, takiej jak obj. Jest to domyślny sposób dostępu do elementu tablicy, ale możemy to również zrobić dla obiektu.

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

Czasami twój obiekt może mieć arbitralną liczbę właściwości bez żadnego określonego kształtu. W takim przypadku można po prostu użyć typu object. Jednak ten typ object definiuje dowolną wartość, która nie jest number, string, boolean, symbol, null, lub undefined, jak omówiono w lekcji o podstawowych typach.

Jeśli potrzebujemy ściśle sprawdzić, czy wartość jest zwykłym obiektem JavaScript, wtedy możemy mieć problem. Można to rozwiązać za pomocą typu interfejsu z sygnaturą indeksu dla nazwy właściwości.

interface SimpleObject {
: any;
}

Interfejs SimpleObject definiuje kształt obiektu z string kluczami, których wartości mogą być any typu danych. Tutaj, nazwa właściwości key jest używana tylko jako placeholder, ponieważ jest zamknięta w nawiasach kwadratowych.

(indexable-type-object.ts)

W powyższym przykładzie, zdefiniowaliśmy ross i monica obiekt typu SimpleObject interfejsu. Ponieważ obiekty te zawierają klucze string i wartości typu danych any, jest to całkowicie legalne.

Jeśli jesteś zdezorientowany co do klucza 1 w monica, który jest typem number, jest to legalne, ponieważ elementy obiektów lub tablic w JavaScript mogą być indeksowane przy użyciu kluczy number lub string, jak pokazano poniżej.

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

Jeśli potrzebujemy być bardziej precyzyjni co do typu kluczy i ich wartości, z pewnością możemy to również zrobić. Na przykład, możemy zdefiniować indeksowalny typ interfejsu z kluczami typu number i wartościami typu number, jeśli chcemy.

💡 Typ klucza sygnatury indeksu musi być albo string albo number.

(indexable-type-number.ts)

W powyższym przykładzie zdefiniowaliśmy LapTimesinterfejs, który może zawierać nazwy właściwości typu number i wartości typu number. Ten interfejs może reprezentować strukturę danych, która może być indeksowana za pomocą kluczy number stąd tablica ross i obiekty monica i joey są legalne.

Jednakże obiekt rachel nie jest zgodny z kształtem LapTimes, ponieważ klucz one jest string i można do niego uzyskać dostęp tylko za pomocą string takich jak rachel i nic innego. Stąd kompilator TypeScript wyrzuci błąd, jak pokazano powyżej.

Możliwe jest posiadanie niektórych właściwości wymaganych i niektórych opcjonalnych w indeksowalnym typie interfejsu. Może to być całkiem przydatne, gdy potrzebujemy, aby obiekt miał określony kształt, ale tak naprawdę nie ma znaczenia, jeśli w obiekcie pojawią się dodatkowe i niechciane właściwości.

(indexable-type-required-properties.ts)

W powyższym przykładzie zdefiniowaliśmy interfejs LapTimes, który musi zawierać właściwość name o wartości string oraz opcjonalną właściwość age o wartości number. Obiekt typu LapTimes może również posiadać dowolne właściwości, których kluczami muszą być number i których wartościami również powinny być number.

Obiekt ross jest poprawnym obiektem LapTimes, mimo że nie posiada właściwości age, ponieważ jest ona opcjonalna. Jednak monica ma właściwość age, ale jej wartością jest string, dlatego nie jest zgodny z interfejsem LapTimes.

Obiekt joey również nie jest zgodny z interfejsem LapTimes, ponieważ ma właściwość gender, która jest typem string. Obiekt rachel nie posiada właściwości name, która jest wymagana w interfejsie LapTimes.

💡 Istnieje kilka gotchas, na które musimy zwrócić uwagę podczas używania typów indeksowalnych. Są one wymienione w tej dokumentacji.

Extending Interface

Podobnie jak klasy, interfejs może dziedziczyć właściwości z innych interfejsów. Jednakże, w przeciwieństwie do klas w JavaScript, interfejs może dziedziczyć z wielu interfejsów. Używamy słowa kluczowego extends do dziedziczenia interfejsu.

(extend-interface.ts)

Przez rozszerzenie interfejsu, interfejs potomny otrzymuje wszystkie właściwości interfejsu nadrzędnego. Jest to całkiem przydatne, gdy wiele interfejsów ma wspólną strukturę i chcemy uniknąć powielania kodu poprzez wyprowadzenie wspólnych właściwości do wspólnego interfejsu, który może być później dziedziczony.

(extend-multiple-interfaces.ts)

W powyższym przykładzie utworzyliśmy interfejs Student, który dziedziczy właściwości po interfejsach Person i Player.

Deklaracje wielu interfejsów

W poprzednim rozdziale dowiedzieliśmy się, w jaki sposób interfejs może dziedziczyć właściwości innego interfejsu. Zostało to zrobione przy użyciu słowa kluczowego extend. Jednakże, gdy interfejsy o tej samej nazwie są zadeklarowane w tym samym module (pliku), TypeScript łączy ich właściwości razem, tak długo jak mają różne nazwy właściwości lub ich sprzeczne typy właściwości są takie same.

(merged-interface.ts)

W powyższym przykładzie zadeklarowaliśmy interfejs Person kilka razy. Dzięki temu uzyskamy pojedynczą deklarację interfejsu Person poprzez scalenie właściwości wszystkich deklaracji interfejsu Person.

💡 W lekcji Classes dowiedzieliśmy się, że klasa class implicite deklaruje interfejs, a interfejs może rozszerzyć ten interfejs. Więc jeśli program ma klasę Person i interfejs Person, to końcowy Person typ (interfejs) będzie miał połączone właściwości między klasą i interfejsem.

Interfejsy zagnieżdżone

Interfejs może mieć głęboko zagnieżdżone struktury. W poniższym przykładzie, pole info interfejsu Student definiuje kształt obiektu o właściwościach firstName i lastName.

(nested-shape.ts)

Podobnie, jest całkowicie legalne, aby pole interfejsu miało typ innego interfejsu. W poniższym przykładzie, pole info interfejsu Student ma typ Person interfejsu.

(nested-interface.ts)

.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.