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.
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.
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.
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.
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
.
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.
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.
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
.
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
nafalse
, 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).
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.
W powyższym przykładzie zdefiniowaliśmy IsSumOdd
interfejs, 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.
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ę.
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.
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
albonumber
.
W powyższym przykładzie zdefiniowaliśmy LapTimes
interfejs, 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.
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.
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.
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.
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 interfejsPerson
, to końcowyPerson
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
.
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.
.