TypeScript
gränssnitt
Sök…
Introduktion
Ett gränssnitt anger en lista över fält och funktioner som kan förväntas i alla klasser som implementerar gränssnittet. Omvänt kan en klass inte implementera ett gränssnitt såvida det inte har alla fält och funktioner som anges i gränssnittet.
Den främsta fördelen med att använda gränssnitt är att det gör att man kan använda objekt av olika slag på ett polymorft sätt. Detta beror på att varje klass som implementerar gränssnittet har åtminstone de fälten och funktionerna.
Syntax
- gränssnitt gränssnittsnamn {
- parameterName: parameterType;
- optionalParameterName ?: parameterType;
- }
Anmärkningar
Gränssnitt mot typ alias
Gränssnitt är bra för att ange formen på ett objekt, t.ex. för ett personobjekt du kan ange
interface person {
id?: number;
name: string;
age: number;
}
Men vad händer om du vill representera, säg, hur en person lagras i en SQL-databas? Eftersom varje DB-post består av en rad med form [string, string, number]
(så en rad strängar eller siffror), finns det inget sätt att du kan representera detta som en objektform, eftersom raden inte har några egenskaper som sådan är det bara en matris.
Detta är ett tillfälle där typerna är användbara. I stället för att specificera i varje funktion som accepterar en function processRow(row: [string, string, number])
kan du skapa ett separat alias för en rad och sedan använda det i varje funktion:
type Row = [string, string, number];
function processRow(row: Row)
Officiell gränssnittsdokumentation
https://www.typescriptlang.org/docs/handbook/interfaces.html
Lägg till funktioner eller egenskaper till ett befintligt gränssnitt
Låt oss anta att vi har en referens till JQuery
typdefinitionen och vi vill utvidga den till att ha ytterligare funktioner från ett plugin som vi inkluderade och som inte har en officiell typdefinition. Vi kan enkelt utöka det genom att deklarera funktioner som läggs till via plugin i en separat gränssnittsdeklaration med samma JQuery
namn:
interface JQuery {
pluginFunctionThatDoesNothing(): void;
// create chainable function
manipulateDOM(HTMLElement): JQuery;
}
Kompilatorn kommer att slå samman alla deklarationer med samma namn till en - se deklaration som slås samman för mer information.
Klassgränssnitt
Förklara public
variabler och metodtyper i gränssnittet för att definiera hur andra typskriptkoder kan interagera med det.
interface ISampleClassInterface {
sampleVariable: string;
sampleMethod(): void;
optionalVariable?: string;
}
Här skapar vi en klass som implementerar gränssnittet.
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;
}
}
Exemplet visar hur man skapar ett gränssnitt ISampleClassInterface
och en klass SampleClass
som implements
gränssnittet.
Utökande gränssnitt
Anta att vi har ett gränssnitt:
interface IPerson {
name: string;
age: number;
breath(): void;
}
Och vi vill skapa ett mer specifikt gränssnitt som har samma egenskaper för personen, vi kan göra det med hjälp av extends
nyckelordet:
interface IManager extends IPerson {
managerId: number;
managePeople(people: IPerson[]): void;
}
Dessutom är det möjligt att utöka flera gränssnitt.
Använda gränssnitt för att verkställa typer
En av de viktigaste fördelarna med Typescript är att det verkställer datatyper av värden som du skickar runt din kod för att förhindra misstag.
Låt oss säga att du skapar en applikation för husdjursdating.
Du har den här enkla funktionen som kontrollerar om två husdjur är kompatibla med varandra ...
checkCompatible(petOne, petTwo) {
if (petOne.species === petTwo.species &&
Math.abs(petOne.age - petTwo.age) <= 5) {
return true;
}
}
Det här är helt funktionell kod, men det skulle vara alldeles för enkelt för någon, särskilt andra som arbetar med den här applikationen som inte skrev denna funktion, att vara medvetna om att de är tänkta att passera den objekt med 'arter' och 'ålder' egenskaper. De kan felaktigt prova checkCompatible(petOne.species, petTwo.species)
och sedan lämnas för att räkna ut de fel som kastas när funktionen försöker komma åt petOne.species.species eller petOne.species.age!
Ett sätt vi kan förhindra att detta sker är att specificera de egenskaper vi vill ha i husdjursparametrarna:
checkCompatible(petOne: {species: string, age: number}, petTwo: {species: string, age: number}) {
//...
}
I det här fallet kommer Typescript att se till att allt som skickas till funktionen har egenskaper för "art" och "ålder" (det är okej om de har ytterligare egenskaper), men det här är lite av en svår lösning, även med endast två egenskaper angivna. Med gränssnitt finns det ett bättre sätt!
Först definierar vi vårt gränssnitt:
interface Pet {
species: string;
age: number;
//We can add more properties if we choose.
}
Nu behöver vi bara ange typen av våra parametrar som vårt nya gränssnitt, som så ...
checkCompatible(petOne: Pet, petTwo: Pet) {
//...
}
... och Typescript kommer att se till att parametrarna som skickas till vår funktion innehåller de egenskaper som anges i Pet-gränssnittet!
Generiska gränssnitt
Precis som klasser kan gränssnitt också ta emot polymorfa parametrar (alias Generics).
Förklara generiska parametrar för gränssnitt
interface IStatus<U> {
code: U;
}
interface IEvents<T> {
list: T[];
emit(event: T): void;
getAll(): T[];
}
Här kan du se att våra två gränssnitt tar några generiska parametrar, T och U.
Implementera generiska gränssnitt
Vi skapar en enkel klass för att implementera gränssnittet 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;
}
}
Låt oss skapa några fall av vår statsklass .
I vårt exempel State
kommer klassen att hantera en generisk status med hjälp IStatus<T>
. På detta sätt kommer gränssnittet IEvent<T>
också att hantera en 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));
Här vår State
klass skrivs som 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);
});
Vår State
klass skrivs som IStatus<Code>
. På detta sätt kan vi överföra mer komplex typ till vår utsläppsmetod.
Som ni ser kan generiska gränssnitt vara ett mycket användbart verktyg för statisk typkod.
Använda gränssnitt för polymorfism
Det främsta skälet till att använda gränssnitt för att uppnå polymorfism och ge utvecklare att implementera på sitt eget sätt i framtiden genom att implementera gränssnittets metoder.
Anta att vi har ett gränssnitt och tre klasser:
interface Connector{
doConnect(): boolean;
}
Detta är anslutningsgränssnitt. Nu kommer vi att implementera det för Wifi-kommunikation.
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
}
}
Här har vi utvecklat vår konkreta klass som heter WifiConnector
som har sin egen implementering. Detta är nu typ Connector
.
Nu skapar vi vår System
som har en komponent Connector
. Detta kallas beroendeinjektion.
export class System {
constructor(private connector: Connector){ #inject Connector type
connector.doConnect()
}
}
constructor(private connector: Connector)
denna linje är mycket viktig här. Connector
är ett gränssnitt och måste ha doConnect()
. Eftersom Connector
är ett gränssnitt har detta System
mycket mer flexibilitet. Vi kan passera alla typer som har implementerat Connector
gränssnitt. I framtiden uppnår utvecklare mer flexibilitet. Till exempel vill nu utvecklaren lägga till Bluetooth-anslutningsmodul:
export class BluetoothConnector implements Connector{
public doConnect(): boolean{
console.log("Connecting via Bluetooth");
console.log("Pair with PIN");
console.log("Connected");
return true
}
}
Se att Wifi och Bluetooth har sin egen implementering. Det finns olika sätt att ansluta. Därför har båda emellertid implementerat Type Connector
de är nu Type Connector
. Så att vi kan överföra någon av dem till System
klassen som konstruktörsparameter. Detta kallas polymorfism. System
är nu inte medvetet om det är Bluetooth / Wifi även vi kan lägga till en annan kommunikationsmodul som Inferade, Bluetooth5 och överhuvudtaget genom att bara implementera Connector
gränssnitt.
Detta kallas Duck typing . Connector
är nu dynamisk eftersom doConnect()
bara är en platshållare och utvecklare implementerar detta som sin egen.
om på constructor(private connector: WifiConnector)
där WifiConnector
är en konkret klass, vad kommer att hända? Då System
klass kommer tätt par endast med WifiConnector inget annat. Här gränssnittet löste vårt problem med polymorfism.
Implicit implementering och objektform
TypeScript stöder gränssnitt, men kompilatorn matar ut JavaScript, vilket inte gör det. Därför förloras gränssnitt effektivt i kompileringssteget. Det är därför typkontroll av gränssnitt förlitar sig på objektets form - vilket betyder om objektet stöder fälten och funktionerna på gränssnittet - och inte om gränssnittet faktiskt är implementerat eller inte.
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);
Så även om Ball
inte uttryckligen implementerar IKickable
, kan en Ball
instans tilldelas (och manipuleras som) en IKickable
, även om typen anges.