Ett gränssnitt är en form av ett objekt. Ett standard JavaScript-objekt är en karta med key:value-par. Nycklar till JavaScript-objekt är i nästan alla fall strängar och deras värden är alla JavaScript-värden som stöds (primitiva eller abstrakta).

Ett gränssnitt talar om för TypeScript-kompilatorn vilka egenskapsnamn ett objekt kan ha och deras motsvarande värdetyper. Därför är gränssnittet en typ och är en abstrakt typ eftersom det består av primitiva typer.

När vi definierar ett objekt med egenskaper (nycklar) och värden skapar TypeScript ett implicit gränssnitt genom att titta på egenskapsnamnen och datatypen för deras värden i objektet. Detta sker på grund av typinferensen.

(object-shape.ts)

I exemplet ovan har vi skapat ett objekt student med fälten firstName, lastName, age och getSalary och tilldelat några inledande värden. Med hjälp av denna information skapar TypeScript en implicit gränssnittstyp för student.

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

Ett gränssnitt är precis som ett objekt, men det innehåller bara information om objektets egenskaper och deras typer. Vi kan också skapa en gränssnittstyp och ge den ett namn så att vi kan använda den för att kommentera objektvärden, men här har gränssnittet inget namn eftersom det skapades implicit. Du kan jämföra detta med funktionstypen i föregående lektion som först skapades implicit och sedan skapade vi en funktionstyp explicit med hjälp av typalias.

Låt oss försöka mixtra med objektegenskaperna efter att de definierats.

(shape-override.ts)

Som du kan se i exemplet ovan kommer TypeScript ihåg formen på ett objekt eftersom typen ross är det implicita gränssnittet. Om vi försöker åsidosätta värdet av en egenskap med ett värde av annan typ än den som anges i gränssnittet eller försöker lägga till en ny egenskap som inte anges i gränssnittet kommer TypeScript-kompilatorn inte att kompilera programmet.

Om du vill att ett objekt i princip ska ha vilken egenskap som helst kan du explicit markera ett värde any och TypeScript-kompilatorn kommer inte att härleda typen från det tilldelade objektvärdet. Det finns andra bättre sätt att uppnå exakt detta och vi kommer att gå igenom dem i den här artikeln.

(shape-override-any.ts)

Interfacedeklaration

Det implicita gränssnittet som vi har sett hittills är visserligen tekniskt sett en typ, men det har inte definierats explicit. Som diskuterats är ett gränssnitt inget annat än den form som ett objekt kan ta. Om du har en funktion som accepterar ett argument som ska vara ett objekt men med en viss form, måste vi annotera det argumentet (parametern) med en gränssnittstyp.

(argument-with-shape.ts)

I exemplet ovan har vi definierat en funktion getPersonInfo som accepterar ett objektargument som har firstName, lastName, age och getSalary fält av angivna datatyper. Observera att vi har använt ett objekt som innehåller egenskapsnamn och deras motsvarande typer som en typ med hjälp av :<type>-annotationen. Detta är ett exempel på ett anonymt gränssnitt eftersom gränssnittet inte har något namn, det användes inline.

Det här verkar lite komplicerat att hantera. Om ross objektet blir mer komplicerat och det måste användas på flera ställen verkar TypeScript bara vara en sak som du gillade från början men som nu bara är en svår sak att hantera. För att lösa det här problemet definierar vi en gränssnittstyp med hjälp av nyckelordet interface.

(argument-with-interface.ts)

I exemplet ovan har vi definierat ett gränssnitt Person som beskriver formen på ett objekt, men den här gången har vi ett namn som vi kan använda för att hänvisa till denna typ. Vi har använt den här typen för att annotera ross-variabeln samt person-argumentet i getPersonIfo-funktionen. Detta kommer att informera TypeScript om att validera dessa enheter mot formen Person.

Varför använda ett gränssnitt?

Interfacetyp kan vara viktigt för att tvinga fram en viss form. Typiskt för JavaScript är att vi vid körningen litar blint på att ett objekt alltid kommer att innehålla en viss egenskap och att den egenskapen alltid kommer att ha ett värde av en viss typ, till exempel {age: 21, ...}.

När vi faktiskt börjar utföra operationer på den egenskapen utan att först kontrollera om egenskapen finns på objektet eller om dess värde är det vi förväntade oss, kan saker och ting gå snett och det kan leda till att programmet blir oanvändbart i efterhand. Till exempel är {age: '21', ...}, här age värdet en string.

Interface ger en säker mekanism för att hantera sådana scenarier vid kompileringstid. Om du av misstag använder en egenskap på ett objekt som inte existerar eller använder värdet av en egenskap i en olaglig operation kommer TypeScript-kompilatorn inte att kompilera ditt program. Låt oss se ett exempel.

(interface-safety.ts)

I exemplet ovan försöker vi använda name-egenskapen name för _student-argumentet inuti printStudent-funktionen. Eftersom _student-argumentet är en typ av Student-gränssnittet, ger TypeScript-kompilatorn ett fel under kompileringen eftersom den här egenskapen inte finns i Student-gränssnittet.

Samma sak är 100 — _student.firstName inte en giltig operation eftersom firstName-egenskapen är en typ av string och sist jag kollade kan man inte subtrahera en string från en number är JavaScript (resulterar i NaN).

I exemplet ovan har vi använt oss av det traditionella sättet att skriva funktionstypen för getSalary-fältet. Du kan dock också använda funktionssyntax utan kroppen för samma sak, vilket i allmänhet används i gränssnitt.

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

Optionella egenskaper

Undertiden behöver ett objekt ha en egenskap som innehåller data av en viss datatyp, men det är inte obligatoriskt att ha den egenskapen på objektet. Detta liknar de valfria funktionsparametrar som vi lärde oss i föregående lektion.

Sådana egenskaper kallas valfria egenskaper. Ett gränssnitt kan innehålla valfria egenskaper och vi använder ?:Type-annotationen för att representera dem, precis som de valfria funktionsparametrarna.

(optional-properties.ts)

I exemplet ovan har gränssnittet Student egenskapen age, som är valfri. Om age-egenskapen tillhandahålls måste den dock ha ett värde av typen number.

I fallet med ross-objektet, som är en typ av Student-gränssnittet, har vi inte tillhandahållit värdet för age-egenskapen, vilket är lagligt, men i fallet med monica har vi tillhandahållit age-egenskapen, men dess värde är string, vilket inte är lagligt. Därför ger TypeScript-kompilatorn ett fel.

Felet kan tyckas konstigt, men det är faktiskt logiskt. Om egenskapen age inte finns på ett objekt kommer object.age att returnera undefined som är en typ av undefined. Om den finns måste värdet vara av typen number.

Därmed kan age-egendomsvärdet antingen vara av typen undefined eller number som i TypeScript representeras med hjälp av union-syntaxen number | undefined.

💡 Vi kommer att lära oss om typunioner i en lektion om Type System.

Obligatoriska egenskaper innebär dock allvarliga problem under programkörningen. Låt oss tänka oss att vi använder egenskapen age i en aritmetisk operation men dess värde är undefined. Detta är ett slags allvarligt problem.

Men det goda är att TypeScript-kompilatorn inte tillåter att olagliga operationer utförs på en valfri egenskap eftersom dess värde kan vara undefined.

(optional-properties-safety.ts)

I exemplet ovan utför vi en aritmetisk operation på egenskapen age, vilket är olagligt eftersom värdet på denna egenskap kan vara number eller undefined under körtiden. Att utföra aritmetiska operationer på undefined resulterar i NaN (inte ett tal).

💡 För ovanstående program var vi dock tvungna att sätta --strictNullChecks-flaggan till false, vilket är en TypeScript-kompilerflagga. Om vi ger det här alternativet kompileras ovanstående program helt okej.

För att undvika det här felet eller den här varningen måste vi uttryckligen tala om för TypeScript-kompilatorn att den här egenskapen är en typ av number och inte number eller undefined. För detta använder vi type assertion (AKA typkonvertering eller typecasting).

(optional-properties-safety-override.ts)

I ovanstående program har vi använt (_student.age as number) som konverterar typen _student.age från number | undefined till number. Detta är ett sätt att tala om för TypeScript-kompilatorn: ”Hej, det här är ett nummer”. Men ett bättre sätt att hantera detta skulle vara att också kontrollera om _student.age är undefined vid körning och sedan utföra den aritmetiska operationen.

💡 Vi kommer att lära oss om typasserbationer i en lektion om Type System.

Funktionstyp med hjälp av ett gränssnitt

När det gäller inte bara formen på ett vanligt objekt, utan ett gränssnitt kan också beskriva signaturen för en funktion. I föregående lektion använde vi typalias för att beskriva en funktionstyp, men gränssnitt kan också göra det.

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

Syntaxen för att deklarera ett gränssnitt som en funktionstyp liknar själva funktionssignaturen. Som du kan se i exemplet ovan innehåller gränssnittets kropp den exakta signaturen för en anonym funktion, utan kroppen förstås. Här spelar parameternamn ingen roll.

(function-signature.ts)

I exemplet ovan har vi definierat IsSumOddgränssnittet som definierar en funktionstyp som tar emot två argument av typen number och returnerar ett booleanvärde. Nu kan du använda den här typen för att beskriva en funktion eftersom gränssnittstypen IsSumOdd är likvärdig med funktionstypen (x: number, y: number) => boolean.

Ett gränssnitt med en anonym metodsignatur beskriver en funktion. Men en funktion i JavaScript-området är också ett objekt, vilket innebär att du kan lägga till egenskaper till ett funktionsvärde precis som ett objekt. Därför är det helt lagligt att du kan definiera vilka egenskaper som helst på ett gränssnitt av funktionstyp.

(function-signature-with-methods.ts)

I exemplet ovan har vi lagt till egenskaperna type och calculate på gränssnittet IsSumOdd som beskriver en funktion. Med hjälp av Object.assign-metoden slår vi samman type– och calculate-egenskaperna med ett funktionsvärde.

Interface av funktionstyp kan vara till hjälp för att beskriva konstruktörsfunktioner. En konstruktorfunktion liknar en klass vars uppgift är att skapa objekt (instanser). Vi hade bara konstruktorfunktioner fram till ES5 för att efterlikna en class i JavaScript. Därför kompilerar TypeScript klasser till konstruktorfunktioner om du riktar dig till ES5 eller lägre.

💡 Om du vill lära dig mer om konstruktorfunktioner kan du följa den här artikeln.

Om vi sätter nyckelordet new före signaturen för anonyma funktioner i gränssnittet gör det funktionen konstruerbar. Det innebär att funktionen endast kan åberopas med hjälp av nyckelordet new för att generera objekt och inte med hjälp av ett vanligt funktionsanrop. Ett exempel på en konstruktorfunktion ser ut som nedan.

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

Tursamt nog behöver vi inte arbeta med konstruktorfunktioner eftersom TypeScript tillhandahåller nyckelordet class för att skapa en klass som är mycket enklare att arbeta med än en konstruktorfunktion, tro mig. Faktum är att en class innerst inne är en konstruktorfunktion i JavaScript. Prova nedanstående exempel.

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

En klass och en konstruktorfunktion är en och samma sak. Den enda skillnaden är att class ger oss en rik OOP-syntax att arbeta med. Därför representerar ett gränssnitt av en konstruktorfunktionstyp en klass.

(function-signature-with-new.ts)

I exemplet ovan har vi definierat klassen Animal med en konstruktorfunktion som accepterar ett argument av typen string. Du kan betrakta detta som en konstruktorfunktion som har en liknande signatur som Animal-konstruktorn.

AnimalInterface definierar en konstruktorfunktion eftersom den har anonymous function prefixerad med nyckelordet new. Detta innebär att klassen Animal kvalificerar sig för att vara en typ av AnimalInterface. Här är AnimalInterface gränssnittstyp likvärdig med funktionstypen new (sound: string) => any.

createAnimalFunktionen createAnimal accepterar ctor argument av AnimalInterface typ, därför kan vi lämna över Animal klass som argumentvärde. Vi kommer inte att kunna lägga till getSound metodsignatur för Animal-klassen i AnimalInterface och orsaken förklaras i lektionen Classes.

Indexerbara typer

Ett indexerbart objekt är ett objekt vars egenskaper kan nås med hjälp av en indexsignatur som obj. Detta är standardmetoden för att komma åt ett arrayelement, men vi kan också göra detta för objektet.

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

I vissa fall kan ditt objekt ha ett godtyckligt antal egenskaper utan någon bestämd form. I det fallet kan du bara använda object typ. Denna object-typ definierar dock alla värden som inte number, string, boolean, symbol, null eller undefined som diskuterades i lektionen om grundläggande typer.

Om vi strikt måste kontrollera om ett värde är ett vanligt JavaScript-objekt så kan vi få ett problem. Detta kan lösas med hjälp av en gränssnittstyp med en indexsignatur för egenskapsnamnet.

interface SimpleObject {
: any;
}

SimpleObjectgränssnittet SimpleObject definierar formen för ett objekt med string nycklar vars värden kan vara any datatyp. Här används key egenskapsnamnet bara som platshållare eftersom det är omslutet av hakparenteser.

(indexable-type-object.ts)

I exemplet ovan har vi definierat ross och monica objekt av typen SimpleObject gränssnitt. Eftersom dessa objekt innehåller string-nycklar och värden av any-datatypen är det helt lagligt.

Om du är förvirrad över nyckeln 1 i monica, som är en typ av number, är detta lagligt eftersom objekt- eller arrayobjekt i JavaScript kan indexeras med hjälp av number– eller string-nycklar, vilket visas nedan.

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

Om vi behöver vara mer exakta när det gäller nyckeltyperna och deras värden kan vi säkert göra det också. Vi kan till exempel definiera en indexerbar gränssnittstyp med nycklar av typen number och värden av typen number om vi vill.

💡 En indexsignaturnyckeltyp måste vara antingen string eller number.

(indexable-type-number.ts)

I exemplet ovan har vi definierat ett LapTimes gränssnitt som kan innehålla egenskapsnamn av typen number och värden av typen number. Detta gränssnitt kan representera en datastruktur som kan indexeras med hjälp av number-nycklar därför är array ross och objekten monica och joey lagliga.

Objektet rachel överensstämmer dock inte med formen för LapTimes eftersom nyckeln one är en string och det kan endast nås med hjälp av string som rachel och ingenting annat. Därför kommer TypeScript-kompilatorn att kasta ett fel som visas ovan.

Det är möjligt att ha vissa egenskaper obligatoriska och andra valfria i en indexerbar gränssnittstyp. Detta kan vara ganska användbart när vi behöver ett objekt som har en viss form men det spelar egentligen ingen roll om vi får extra och oönskade egenskaper i objektet.

(indexable-type-required-properties.ts)

I exemplet ovan har vi definierat ett gränssnitt LapTimes som måste innehålla egenskapen name med string värde och den valfria egenskapen age med number värde. Ett objekt av typen LapTimes kan också ha godtyckliga egenskaper vars nycklar måste vara number och vars värden också ska vara number.

Objektet ross är ett giltigt LapTimes-objekt även om det inte har egenskapen age eftersom den är valfri. monica har dock egenskapen age men dess värde är string och uppfyller därför inte gränssnittet LapTimes.

Objektet joey uppfyller inte heller gränssnittet LapTimes eftersom det har egenskapen gender som är en typ av string. rachel-objektet har inte name-egenskap som krävs i LapTimes-gränssnittet.

💡 Det finns några getchas som vi måste se upp för när vi använder indexerbara typer. Dessa nämns i den här dokumentationen.

Extending Interface

Likt klasser kan ett gränssnitt ärva egenskaper från andra gränssnitt. Men till skillnad från klasser i JavaScript kan ett gränssnitt ärva från flera gränssnitt. Vi använder nyckelordet extends för att ärva ett gränssnitt.

(extend-interface.ts)

Om ett gränssnitt utökas får det underordnade gränssnittet alla egenskaper hos det överordnade gränssnittet. Detta är ganska användbart när flera gränssnitt har en gemensam struktur och vi vill undvika koddubbling genom att ta ut de gemensamma egenskaperna i ett gemensamt gränssnitt som senare kan ärvas.

(extend-multiple-interfaces.ts)

I exemplet ovan har vi skapat ett Student gränssnitt som ärver egenskaper från Person och Player gränssnittet.

Multiple Interface Declarations

I föregående avsnitt lärde vi oss hur ett gränssnitt kan ärva egenskaper från ett annat gränssnitt. Detta gjordes med hjälp av nyckelordet extend. När gränssnitt med samma namn deklareras i samma modul (fil) slår TypeScript dock samman deras egenskaper så länge de har olika egenskapsnamn eller deras konfliktfyllda egenskapstyper är desamma.

(merged-interface.ts)

I exemplet ovan har vi deklarerat Person gränssnitt flera gånger. Detta kommer att resultera i en enda Person gränssnittsdeklaration genom att egenskaperna för alla Person gränssnittsdeklarationer slås samman.

💡 I lektionen Classes har vi lärt oss att en class implicit deklarerar ett gränssnitt och att ett gränssnitt kan utöka det gränssnittet. Så om ett program har en klass Person och ett gränssnitt Person kommer den slutliga Person typen (gränssnittet) att ha sammanslagna egenskaper mellan klassen och gränssnittet.

Nested Interfaces

Ett gränssnitt kan ha djupt nischade strukturer. I exemplet nedan definierar info-fältet i Student-gränssnittet formen för ett objekt med firstName– och lastName-egenskaper.

(nested-shape.ts)

På samma sätt är det helt lagligt för ett fält i ett gränssnitt att ha typen för ett annat gränssnitt. I följande exempel har info-fältet i Student-gränssnittet typen Person-gränssnittet.

(nested-interface.ts)

Lämna ett svar

Din e-postadress kommer inte publiceras.