En grænseflade er en form af et objekt. Et standard JavaScript-objekt er et kort af key:value-par. JavaScript-objektnøgler er i næsten alle tilfælde strenge, og deres værdier er alle understøttede JavaScript-værdier (primitive eller abstrakte).

En grænseflade fortæller TypeScript-compileren om egenskabsnavne, som et objekt kan have, og deres tilsvarende værdityper. Derfor er interface en type og er en abstrakt type, da den er sammensat af primitive typer.

Når vi definerer et objekt med egenskaber (nøgler) og værdier, opretter TypeScript en implicit grænseflade ved at se på egenskabsnavnene og datatypen for deres værdier i objektet. Dette sker på grund af typeinferencen.

(object-shape.ts)

I ovenstående eksempel har vi oprettet et objekt student med firstName, lastName, age og getSalary felter og tildelt nogle indledende værdier. Ved hjælp af disse oplysninger opretter TypeScript en implicit interfacetype for student.

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

En grænseflade er ligesom et objekt, men den indeholder kun oplysninger om objektets egenskaber og deres typer. Vi kan også oprette en interfacetype og give den et navn, så vi kan bruge den til at annotere objektværdier, men her har denne interface ikke et navn, da den blev oprettet implicit. Du kan sammenligne dette med funktionstypen i den foregående lektion, som først blev oprettet implicit, og derefter oprettede vi en funktionstype eksplicit ved hjælp af typealias.

Lad os prøve at rode med objektets egenskaber, efter at den er blevet defineret.

(shape-override.ts)

Som du kan se af ovenstående eksempel, husker TypeScript formen på et objekt, da typen ross er den implicitte grænseflade. Hvis vi forsøger at overskrive værdien af en egenskab med en værdi af en anden type end den, der er angivet i grænsefladen, eller forsøger at tilføje en ny egenskab, som ikke er angivet i grænsefladen, vil TypeScript-compileren ikke kompilere programmet.

Hvis du ønsker, at et objekt grundlæggende skal have en hvilken som helst egenskab, kan du eksplicit markere en værdi any, og TypeScript-compileren vil ikke udlede typen fra den tildelte objektværdi. Der er andre bedre måder at opnå præcis dette på, og vi vil gennemgå dem i denne artikel.

(shape-override-any.ts)

Interface-deklaration

Den implicitte grænseflade, vi har set indtil nu, er teknisk set en type, men den blev ikke defineret eksplicit. Som diskuteret er en grænseflade ikke andet end den form, som et objekt kan tage. Hvis du har en funktion, der accepterer et argument, som skal være et objekt, men af en bestemt form, skal vi annotere dette argument (parameter) med en interfacetype.

(argument-med-form.ts)

I ovenstående eksempel har vi defineret en funktion getPersonInfo, som accepterer et objektargument, der har firstName, lastName, age og getSalary felter af specificerede datatyper. Bemærk, at vi har brugt et objekt, der indeholder egenskabsnavne og deres tilsvarende typer som en type ved hjælp af :<type>-annotationen. Dette er et eksempel på en anonym grænseflade, da grænsefladen ikke har et navn, den blev brugt inline.

Det hele virker lidt kompliceret at håndtere. Hvis ross objektet bliver mere kompliceret, og det skal bruges flere steder, virker TypeScript bare som en ting, som du kunne lide i starten, men som nu bare er en svær ting at håndtere. For at løse dette problem definerer vi en grænsefladetype ved hjælp af nøgleordet interface.

(argument-with-interface.ts)

I eksemplet ovenfor har vi defineret en grænseflade Person, der beskriver formen på et objekt, men denne gang har vi et navn, som vi kan bruge til at henvise til denne type. Vi har brugt denne type til at annotere ross-variablen samt person-argumentet i getPersonIfo-funktionen. Dette vil informere TypeScript om at validere disse enheder i forhold til formen Person.

Hvorfor bruge en grænseflade?

Interfacetype kan være vigtig for at håndhæve en bestemt form. Typisk i JavaScript stoler vi ved kørselstid blindt på, at et objekt altid vil indeholde en bestemt egenskab, og at denne egenskab altid vil have en værdi af en bestemt type som {age: 21, ...} som eksempel.

Når vi rent faktisk begynder at udføre operationer på denne egenskab uden først at kontrollere, om denne egenskab findes på objektet, eller om dens værdi er den, vi forventede, kan det gå galt, og det kan gøre din applikation ubrugelig bagefter. F.eks. er {age: '21', ...}, her age værdien en string.

Interfaces giver en sikker mekanisme til at håndtere sådanne scenarier på kompileringstidspunktet. Hvis du ved et uheld bruger en egenskab på et objekt, der ikke findes, eller bruger værdien af en egenskab i den ulovlige operation, vil TypeScript-kompileren ikke kompilere dit program. Lad os se et eksempel.

(interface-safety.ts)

I ovenstående eksempel forsøger vi at bruge name-egenskaben for _student-argumentet inde i printStudent-funktionen. Da _student-argumentet er en type af Student-grænsefladen, giver TypeScript-kompileren en fejl under kompileringen, da denne egenskab ikke findes i Student-grænsefladen.

Sådan er 100 — _student.firstName heller ikke en gyldig operation, da firstName-egenskaben er en type af string, og sidste gang jeg tjekkede, kan man ikke trække en string fra en number er JavaScript (resulterer i NaN).

I ovenstående eksempel har vi brugt den traditionelle måde at skrive funktionstype for getSalary-feltet på. Du kan dog også bruge funktionssyntaks uden krop til det samme, hvilket generelt bruges i grænseflader.

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

Optionelle egenskaber

I nogle tilfælde har du brug for, at et objekt har en egenskab, der indeholder data af en bestemt datatype, men det er ikke obligatorisk at have denne egenskab på objektet. Dette svarer til de valgfrie funktionsparametre, som vi lærte i den foregående lektion.

Sådanne egenskaber kaldes valgfrie egenskaber. En grænseflade kan indeholde valgfrie egenskaber, og vi bruger ?:Type-annotationen til at repræsentere dem, ligesom de valgfrie funktionsparametre.

(optional-properties.ts)

I ovenstående eksempel har Student-grænsefladen age-egenskaben, som er valgfri. Men hvis age-egenskaben er angivet, skal den have en værdi af typen number.

I tilfældet med ross-objektet, som er en type af Student-grænsefladen, har vi ikke angivet værdien for age-egenskaben, hvilket er lovligt, men i tilfældet med monica har vi angivet age-egenskaben, men dens værdi er string, hvilket ikke er lovligt. Derfor kaster TypeScript-kompileren en fejl.

Fejlen kan virke underlig, men den giver faktisk mening. Hvis age-egenskaben ikke findes på et objekt, vil object.age returnere undefined, som er en type af undefined. Hvis den findes, skal værdien være af typen number.

Hvorfor age-egenskabsværdien kan enten være af typen undefined eller number, som i TypeScript repræsenteres ved hjælp af union-syntaksen number | undefined.

💡 Vi vil lære typeunioner i en Type System-lektion.

Men valgfrie egenskaber giver imidlertid alvorlige problemer under programudførelsen. Lad os forestille os, at vi bruger age-egenskaben i en aritmetisk operation, men dens værdi er undefined. Dette er en slags alvorligt problem.

Men det gode er, at TypeScript-kompileren ikke tillader at udføre ulovlige operationer på en valgfri egenskab, da dens værdi kan være undefined.

(optional-properties-safety.ts)

I ovenstående eksempel udfører vi en aritmetisk operation på age-egenskaben, hvilket er ulovligt, fordi værdien af denne egenskab kan være number eller undefined i runtime. Udførelse af aritmetiske operationer på undefined resulterer i NaN (ikke et tal).

💡 Men for ovenstående program skulle vi tp indstille --strictNullChecks-flaget til false, som er et TypeScript-kompilerflag. Hvis vi giver denne mulighed, kompilerer ovenstående program fint.

For at undgå denne fejl eller advarsel skal vi udtrykkeligt fortælle TypeScript-compileren, at denne egenskab er en type af number og ikke number eller undefined. Til dette bruger vi type assertion (AKA type konvertering eller typecasting).

(optional-properties-safety-override.ts)

I ovenstående program har vi brugt (_student.age as number), som konverterer typen af _student.age fra number | undefined til number. Dette er en måde at fortælle TypeScript-compileren: “Hey, dette er et tal”. Men en bedre måde at håndtere dette på ville være også at kontrollere, om _student.age er undefined på runtime og derefter udføre den aritmetiske operation.

💡 Vi lærer om typeassertions i en Type System-lektion.

Funktionstype ved hjælp af en grænseflade

Nu kan en grænseflade ikke kun beskrive formen af et almindeligt objekt, men også signaturen af en funktion. I den foregående lektion brugte vi typealias til at beskrive en funktionstype, men det kan grænseflader også gøre det.

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

Syntaksen til at deklarere en grænseflade som en funktionstype svarer til selve funktionssignaturen. Som du kan se i eksemplet ovenfor, indeholder grænsefladens krop den nøjagtige signatur for en anonym funktion, selvfølgelig uden kroppen. Her er parameternavne ligegyldige.

(function-signature.ts)

I eksemplet ovenfor har vi defineret IsSumOdd grænseflade, som definerer en funktionstype, der accepterer to argumenter af typen number og returnerer en boolean værdi. Nu kan du bruge denne type til at beskrive en funktion, fordi IsSumOdd-interfacetypen svarer til funktionstypen (x: number, y: number) => boolean.

En grænseflade med en anonym metodesignatur beskriver en funktion. Men en funktion i JavaScript-regi er også et objekt, hvilket betyder, at du kan tilføje egenskaber til en funktionsværdi på samme måde som et objekt. Derfor er det helt lovligt, at du kan definere alle egenskaber på en grænseflade af funktionstypen.

(function-signature-with-methods.ts)

I eksemplet ovenfor har vi tilføjet type og calculate egenskaber på grænsefladen IsSumOdd, som beskriver en funktion. Ved hjælp af Object.assign-metoden sammenføjer vi type– og calculate-egenskaberne med en funktionsværdi.

Interfaces af funktionstypen kan være nyttige til at beskrive konstruktorfunktioner. En konstruktorfunktion svarer til en klasse, hvis opgave er at oprette objekter (instanser). Vi havde kun konstruktorfunktioner frem til ES5 for at efterligne en class i JavaScript. Derfor kompilerer TypeScript klasser til konstruktorfunktioner, hvis du er målrettet ES5 eller derunder.

💡 Hvis du vil lære mere om konstruktorfunktioner, kan du følge denne artikel.

Hvis vi sætter new nøgleordet før anonyme funktionssignaturer i grænsefladen, gør det funktionen konstruerbar. Det betyder, at funktionen kun kan påberåbes ved hjælp af new-nøgleordet for at generere objekter og ikke ved hjælp af et almindeligt funktionskald. En eksempel på en konstruktorfunktion ser ud som nedenfor.

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

Glædigvis behøver vi ikke at arbejde med konstruktorfunktioner, da TypeScript tilbyder class-keyword til at oprette en klasse, som er meget nemmere at arbejde med end en konstruktorfunktion, tro mig. Faktisk er en class dybt nede en konstruktørfunktion i JavaScript. Prøv nedenstående eksempel.

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

En klasse og en konstruktorfunktion er en og samme ting. Den eneste forskel er, at class giver os en rig OOP-syntaks at arbejde med. Derfor repræsenterer en grænseflade af en konstruktorfunktionstype en klasse.

(function-signature-with-new.ts)

I ovenstående eksempel har vi defineret Animal-klassen med en konstruktorfunktion, der accepterer et argument af typen string. Du kan betragte dette som en konstruktorfunktion, der har en lignende signatur som Animal-konstruktøren.

Den AnimalInterface definerer en konstruktorfunktion, da den har anonymous function præfikseret med nøgleordet new. Det betyder, at Animal-klassen kvalificerer sig til at være en type af AnimalInterface. Her svarer AnimalInterface-interfacetype til funktionstypen new (sound: string) => any.

Funktionen createAnimal accepterer ctor argument af typen AnimalInterface, og derfor kan vi videregive Animal-klassen som argumentværdi. Vi vil ikke kunne tilføje getSound metodesignatur for Animal-klassen i AnimalInterface, og årsagen hertil er forklaret i lektionen om klasser.

Indekserbare typer

Et indekserbart objekt er et objekt, hvis egenskaber kan tilgås ved hjælp af en indekssignatur som obj. Dette er standardmetoden til at få adgang til et array-element, men vi kan også gøre dette for objektet.

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

Dit objekt kan til tider have et vilkårligt antal egenskaber uden nogen bestemt form. I det tilfælde kan du bare bruge object typen. Denne object-type definerer dog enhver værdi, som ikke number, string, boolean, symbol, null eller undefined som beskrevet i lektionen om grundlæggende typer.

Hvis vi har brug for strengt at kontrollere, om en værdi er et almindeligt JavaScript-objekt, så kan vi have et problem. Dette kan løses ved hjælp af en grænsefladetype med en indekssignatur for egenskabsnavnet.

interface SimpleObject {
: any;
}

SimpleObjectgrænsefladen SimpleObject definerer formen af et objekt med string nøgler, hvis værdier kan være any datatype. Her bruges key-egenskabsnavnet blot som placeholder, da det er omsluttet af firkantede parenteser.

(indexable-type-object.ts)

I ovenstående eksempel har vi defineret ross og monica objekt af typen SimpleObject-grænseflade. Da disse objekter indeholder string-nøgler og værdier af any-datatatypen, er det helt lovligt.

Hvis du er forvirret over nøglen 1 i monica, som er en type af number, er det lovligt, da objekt- eller array-elementer i JavaScript kan indekseres ved hjælp af number– eller string-nøgler, som vist nedenfor.

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

Hvis vi har brug for at være mere præcise med hensyn til typen af nøgler og deres værdier, kan vi helt sikkert også gøre det. Vi kan f.eks. definere en indekserbar grænsefladetype med nøgler af typen number og værdier af typen number, hvis vi ønsker det.

💡 En indekssignaturnøgletype skal være enten string eller number.

(indexable-type-number.ts)

I eksemplet ovenfor har vi defineret en LapTimes grænseflade, der kan indeholde egenskabsnavne af typen number og værdier af typen number. Denne grænseflade kan repræsentere en datastruktur, der kan indekseres ved hjælp af number-nøgler, hvorfor array ross og objekter monica og joey er lovlige.

Objektet rachel er imidlertid ikke i overensstemmelse med formen af LapTimes, da nøglen one er en string, og der kan kun tilgås ved hjælp af string som rachel og intet andet. Derfor vil TypeScript-kompileren kaste en fejl som vist ovenfor.

Det er muligt at have nogle egenskaber obligatoriske og nogle valgfrie i en indekserbar grænsefladetype. Dette kan være ganske nyttigt, når vi har brug for, at et objekt skal have en bestemt form, men det er egentlig ligegyldigt, om vi får ekstra og uønskede egenskaber i objektet.

(indexable-type-required-properties.ts)

I eksemplet ovenfor har vi defineret en grænseflade LapTimes, som skal indeholde egenskaben name med string-værdi og den valgfrie egenskab age med number-værdi. Et objekt af typen LapTimes kan også have vilkårlige egenskaber, hvis nøgler skal være number, og hvis værdier også skal være number.

Objektet ross er et gyldigt LapTimes-objekt, selv om det ikke har age-egenskaben, da den er valgfri. monica har imidlertid age-egenskaben, men dens værdi er string, hvorfor den ikke er i overensstemmelse med LapTimes-grænsefladen.

Objektet joey er heller ikke i overensstemmelse med LapTimes-grænsefladen, da det har en gender-egenskab, som er en type string. rachel-objektet har ikke name-egenskaben, som er påkrævet i LapTimes-grænsefladen.

💡 Der er nogle gotchas, som vi skal være opmærksomme på, når vi bruger indekserbare typer. De er nævnt i denne dokumentation.

Extending Interface

Som klasser kan en grænseflade arve egenskaber fra andre grænseflader. I modsætning til klasser i JavaScript kan en grænseflade dog arve fra flere grænseflader. Vi bruger nøgleordet extends til at arve en grænseflade.

(extend-interface.ts)

Gennem udvidelse af en grænseflade får den underordnede grænseflade alle egenskaberne fra den overordnede grænseflade. Dette er ganske nyttigt, når flere grænseflader har en fælles struktur, og vi ønsker at undgå kodedobling ved at tage de fælles egenskaber ud i en fælles grænseflade, som senere kan arves.

(extend-multiple-interfaces.ts)

I ovenstående eksempel har vi oprettet en Student grænseflade, der arver egenskaber fra Person og Player grænsefladen.

Multiple Interface Declarations

I det foregående afsnit lærte vi, hvordan en grænseflade kan arve egenskaberne fra en anden grænseflade. Dette blev gjort ved hjælp af nøgleordet extend. Men når grænseflader med samme navn er erklæret i samme modul (fil), fletter TypeScript deres egenskaber sammen, så længe de har forskellige egenskabsnavne, eller deres modstridende egenskabstyper er de samme.

(merged-interface.ts)

I ovenstående eksempel har vi erklæret Persongrænseflade flere gange. Dette vil resultere i en enkelt Person-interfacedeklaration ved at sammenlægge egenskaberne for alle Person-interfacedeklarationerne.

💡 I lektionen Classes har vi lært, at en class implicit deklarerer en grænseflade, og at en grænseflade kan udvide denne grænseflade. Så hvis et program har en klasse Person og en grænseflade Person, vil den endelige Person type (grænseflade) have sammenlagte egenskaber mellem klassen og grænsefladen.

Nested Interfaces

En grænseflade kan have dybt indlejrede strukturer. I eksemplet nedenfor definerer info-feltet i Student-interfacet info formen på et objekt med firstName og lastName egenskaber.

(nested-shape.ts)

Det er ligeledes helt lovligt, at et felt i et interface kan have typen af et andet interface. I følgende eksempel har info-feltet i Student-grænsefladen info typen Persongrænseflade.

(nested-interface.ts)

Skriv et svar

Din e-mailadresse vil ikke blive publiceret.