TypeScript
Interfejsy
Szukaj…
Wprowadzenie
Interfejsy określają listę pól i funkcji, których można oczekiwać od dowolnej klasy implementującej interfejs. I odwrotnie, klasa nie może zaimplementować interfejsu, chyba że ma wszystkie pola i funkcje określone w interfejsie.
Podstawową zaletą korzystania z interfejsów jest to, że pozwala na korzystanie z obiektów różnych typów w sposób polimorficzny. Jest tak, ponieważ każda klasa implementująca interfejs ma co najmniej te pola i funkcje.
Składnia
- interfaceNazwa interfejsu {
- parametrName: parameterType;
- opcjonalneParameterName ?: parameterType;
- }
Uwagi
Interfejsy a aliasy typu
Interfejsy są dobre do określania kształtu obiektu, np. Dla obiektu osoby, który możesz określić
interface person {
id?: number;
name: string;
age: number;
}
Co jednak zrobić, jeśli chcesz reprezentować, powiedzmy, sposób, w jaki osoba jest przechowywana w bazie danych SQL? Ponieważ każdy wpis DB składa się z wiersza kształtu [string, string, number]
(czyli tablica ciągów lub liczb), nie ma możliwości przedstawienia tego jako kształtu obiektu, ponieważ wiersz nie ma żadnych właściwości jako taki, to tylko tablica.
Jest to okazja, gdy przydatne są typy. Zamiast określać w każdej funkcji, która akceptuje parametr parametru rzędu function processRow(row: [string, string, number])
, możesz utworzyć osobny alias typu dla wiersza, a następnie użyć go w każdej funkcji:
type Row = [string, string, number];
function processRow(row: Row)
Oficjalna dokumentacja interfejsu
https://www.typescriptlang.org/docs/handbook/interfaces.html
Dodaj funkcje lub właściwości do istniejącego interfejsu
Załóżmy, że mamy odniesienie do definicji typu JQuery
i chcemy ją rozszerzyć, aby zawierała dodatkowe funkcje z dołączonej wtyczki, która nie ma oficjalnej definicji typu. Możemy go łatwo rozszerzyć, deklarując funkcje dodane przez wtyczkę w osobnej deklaracji interfejsu o tej samej nazwie JQuery
:
interface JQuery {
pluginFunctionThatDoesNothing(): void;
// create chainable function
manipulateDOM(HTMLElement): JQuery;
}
Kompilator połączy wszystkie deklaracje o tej samej nazwie w jedną - więcej informacji można znaleźć w części Scalanie deklaracji .
Interfejs klasy
Zadeklaruj public
zmienne i metody w interfejsie, aby zdefiniować, w jaki sposób inny kod maszynowy może z nim współdziałać.
interface ISampleClassInterface {
sampleVariable: string;
sampleMethod(): void;
optionalVariable?: string;
}
Tutaj tworzymy klasę, która implementuje interfejs.
class SampleClass implements ISampleClassInterface {
public sampleVariable: string;
private answerToLifeTheUniverseAndEverything: number;
constructor() {
this.sampleVariable = 'string value';
this.answerToLifeTheUniverseAndEverything = 42;
}
public sampleMethod(): void {
// do nothing
}
private answer(q: any): number {
return this.answerToLifeTheUniverseAndEverything;
}
}
Przykład pokazuje, jak utworzyć interfejs ISampleClassInterface
i klasę SampleClass
która implements
interfejs.
Rozszerzanie interfejsu
Załóżmy, że mamy interfejs:
interface IPerson {
name: string;
age: number;
breath(): void;
}
Chcemy stworzyć bardziej szczegółowy interfejs, który ma takie same właściwości osoby, możemy to zrobić za pomocą słowa kluczowego extends
:
interface IManager extends IPerson {
managerId: number;
managePeople(people: IPerson[]): void;
}
Ponadto istnieje możliwość rozszerzenia wielu interfejsów.
Używanie interfejsów do wymuszania typów
Jedną z głównych zalet Typescript jest wymuszanie typów danych wartości przekazywanych w kodzie, aby zapobiec błędom.
Załóżmy, że tworzysz aplikację randkową dla zwierząt domowych.
Masz tę prostą funkcję, która sprawdza, czy dwa zwierzaki są ze sobą kompatybilne ...
checkCompatible(petOne, petTwo) {
if (petOne.species === petTwo.species &&
Math.abs(petOne.age - petTwo.age) <= 5) {
return true;
}
}
Jest to w pełni funkcjonalny kod, ale byłoby zbyt łatwe dla kogoś, zwłaszcza innych osób pracujących nad tą aplikacją, które nie napisały tej funkcji, aby nie wiedziały, że powinny przekazywać jej obiekty z „gatunkiem” i „wiekiem” nieruchomości. Mogą przez pomyłkę wypróbować checkCompatible(petOne.species, petTwo.species)
a następnie zostaną pozostawieni, aby dowiedzieć się o błędach checkCompatible(petOne.species, petTwo.species)
gdy funkcja próbuje uzyskać dostęp do petOne.species.species lub petOne.species.age!
Jednym ze sposobów, aby temu zapobiec, jest określenie pożądanych właściwości parametrów zwierzaka:
checkCompatible(petOne: {species: string, age: number}, petTwo: {species: string, age: number}) {
//...
}
W takim przypadku Typescript upewni się, że wszystko przekazane do funkcji ma właściwości „gatunki” i „wiek” (jest w porządku, jeśli mają dodatkowe właściwości), ale jest to trochę niewygodne rozwiązanie, nawet jeśli określono tylko dwie właściwości. Dzięki interfejsom istnieje lepszy sposób!
Najpierw definiujemy nasz interfejs:
interface Pet {
species: string;
age: number;
//We can add more properties if we choose.
}
Teraz wszystko, co musimy zrobić, to określić typ naszych parametrów jako naszego nowego interfejsu, tak jak ...
checkCompatible(petOne: Pet, petTwo: Pet) {
//...
}
... a Typescript upewni się, że parametry przekazane do naszej funkcji zawierają właściwości określone w interfejsie Pet!
Ogólne interfejsy
Podobnie jak klasy, interfejsy mogą również odbierać parametry polimorficzne (znane również jako Generics).
Deklarowanie ogólnych parametrów interfejsów
interface IStatus<U> {
code: U;
}
interface IEvents<T> {
list: T[];
emit(event: T): void;
getAll(): T[];
}
Tutaj możesz zobaczyć, że nasze dwa interfejsy przyjmują ogólne parametry, T i U.
Implementowanie ogólnych interfejsów
Stworzymy prostą klasę w celu implementacji interfejsu IEvents .
class State<T> implements IEvents<T> {
list: T[];
constructor() {
this.list = [];
}
emit(event: T): void {
this.list.push(event);
}
getAll(): T[] {
return this.list;
}
}
Stwórzmy przykłady naszej klasy państwowej .
W naszym przykładzie klasa State
będzie obsługiwać ogólny status za pomocą IStatus<T>
. W ten sposób interfejs IEvent<T>
będzie także obsługiwał IStatus<T>
.
const s = new State<IStatus<number>>();
// The 'code' property is expected to be a number, so:
s.emit({ code: 200 }); // works
s.emit({ code: '500' }); // type error
s.getAll().forEach(event => console.log(event.code));
Tutaj nasza klasa State
jest wpisana jako ISatus<number>
.
const s2 = new State<IStatus<Code>>();
//We are able to emit code as the type Code
s2.emit({ code: { message: 'OK', status: 200 } });
s2.getAll().map(event => event.code).forEach(event => {
console.log(event.message);
console.log(event.status);
});
Nasza klasa State
ma IStatus<Code>
. W ten sposób jesteśmy w stanie przekazać bardziej złożony typ do naszej metody emisji.
Jak widać, ogólne interfejsy mogą być bardzo przydatnym narzędziem do statycznego pisania kodu.
Używanie interfejsów do polimorfizmu
Głównym powodem korzystania z interfejsów w celu osiągnięcia polimorfizmu i zapewnienia programistom wdrożenia na własną rękę w przyszłości poprzez wdrożenie metod interfejsu.
Załóżmy, że mamy interfejs i trzy klasy:
interface Connector{
doConnect(): boolean;
}
To jest interfejs złącza. Teraz zaimplementujemy to do komunikacji Wi-Fi.
export class WifiConnector implements Connector{
public doConnect(): boolean{
console.log("Connecting via wifi");
console.log("Get password");
console.log("Lease an IP for 24 hours");
console.log("Connected");
return true
}
}
Tutaj opracowaliśmy naszą konkretną klasę o nazwie WifiConnector
która ma własną implementację. To jest teraz typ Connector
.
Teraz tworzymy nasz System
który ma komponent Connector
. Nazywa się to wstrzykiwaniem zależności.
export class System {
constructor(private connector: Connector){ #inject Connector type
connector.doConnect()
}
}
constructor(private connector: Connector)
Ta linia jest tutaj bardzo ważna. Connector
jest interfejsem i musi mieć doConnect()
. Ponieważ Connector
jest interfejsem, ta klasa System
ma znacznie większą elastyczność. Możemy przekazać dowolny typ, który ma zaimplementowany interfejs Connector
. W przyszłości programista osiąga większą elastyczność. Na przykład teraz programista chce dodać moduł połączenia Bluetooth:
export class BluetoothConnector implements Connector{
public doConnect(): boolean{
console.log("Connecting via Bluetooth");
console.log("Pair with PIN");
console.log("Connected");
return true
}
}
Zobacz, że Wi-Fi i Bluetooth mają własną implementację. Istnieje inny sposób połączenia. Jednak stąd zarówno wprowadziły Rodzaj Connector
są teraz Rodzaj Connector
. Abyśmy mogli przekazać dowolną z nich do klasy System
jako parametr konstruktora. Nazywa się to polimorfizmem. Klasa System
nie wie teraz, czy jest to Bluetooth / Wi-Fi, nawet możemy dodać kolejny moduł komunikacyjny, taki jak Inferade, Bluetooth5 i cokolwiek innego, po prostu implementując interfejs Connector
.
Nazywa się to pisaniem kaczek . Typ Connector
jest teraz dynamiczny, ponieważ doConnect()
jest tylko symbolem zastępczym, a programista implementuje go jako swój własny.
jeśli przy constructor(private connector: WifiConnector)
gdzie WifiConnector
jest konkretną klasą, co się stanie? Wtedy klasa System
będzie ściśle łączyć się tylko z WifiConnector i niczym więcej. Interfejs rozwiązał nasz problem poprzez polimorfizm.
Implementacja niejawna i kształt obiektu
TypeScript obsługuje interfejsy, ale kompilator generuje JavaScript, co nie. Dlatego interfejsy są skutecznie tracone na etapie kompilacji. Dlatego sprawdzanie typów interfejsów zależy od kształtu obiektu - co oznacza, czy obiekt obsługuje pola i funkcje interfejsu - a nie od tego, czy interfejs jest rzeczywiście zaimplementowany, czy nie.
interface IKickable {
kick(distance: number): void;
}
class Ball {
kick(distance: number): void {
console.log("Kicked", distance, "meters!");
}
}
let kickable: IKickable = new Ball();
kickable.kick(40);
Więc nawet jeśli Ball
nie implementuje jawnie IKickable
, instancja Ball
może zostać przypisana (i zmanipulowana jako) IKickable
, nawet jeśli określony jest typ.