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.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow