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.
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.
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.
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.
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
.
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.
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.
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
.
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 tillfalse
, 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).
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.
I exemplet ovan har vi definierat IsSumOdd
gränssnittet som definierar en funktionstyp som tar emot två argument av typen number
och returnerar ett boolean
vä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.
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.
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
.
createAnimal
Funktionen 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;
}
SimpleObject
grä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.
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
ellernumber
.
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.
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.
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.
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.
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 klassPerson
och ett gränssnittPerson
kommer den slutligaPerson
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.
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.