Een interface is een vorm van een object. Een standaard JavaScript-object is een map van key:value
-paren. JavaScript object sleutels zijn in bijna alle gevallen strings en hun waarden zijn alle ondersteunde JavaScript waarden (primitief of abstract).
Een interface vertelt de TypeScript compiler over de namen van eigenschappen die een object kan hebben en hun overeenkomstige waardetypes. Daarom is een interface een type en is het een abstract type omdat het is samengesteld uit primitieve types.
Wanneer we een object definiëren met eigenschappen (sleutels) en waarden, creëert TypeScript een impliciete interface door te kijken naar de namen van de eigenschappen en het datatype van hun waarden in het object. Dit gebeurt vanwege de type-inferentie.
In het bovenstaande voorbeeld hebben we een object student
gemaakt met firstName
, lastName
, age
en getSalary
velden en een aantal initiële waarden toegewezen. Met behulp van deze informatie maakt TypeScript een impliciet interface type voor student
.
{
firstName: string;
lastName: string;
age: number;
getSalary: (base: number) => number;
}
Een interface is net als een object, maar het bevat alleen de informatie over object eigenschappen en hun typen. We kunnen ook een interface-type maken en het een naam geven, zodat we het kunnen gebruiken om objectwaarden te annoteren, maar hier heeft deze interface geen naam omdat hij impliciet is gemaakt. Je kunt dit vergelijken met het functietype in de vorige les dat eerst impliciet was gecreëerd en daarna hebben we een functietype expliciet gemaakt met behulp van type alias.
Laten we eens proberen om te rommelen met de objecteigenschappen nadat het is gedefinieerd.
Zoals je in het bovenstaande voorbeeld kunt zien, onthoudt TypeScript de vorm van een object omdat het type ross
de impliciete interface is. Als we proberen de waarde van een eigenschap te overschrijven met een waarde van een ander type dan wat in de interface gespecificeerd is of als we proberen een nieuwe eigenschap toe te voegen die niet in de interface gespecificeerd is, dan zal de TypeScript compiler het programma niet compileren.
Als je wilt dat een object in principe elke eigenschap heeft, dan kun je expliciet een waarde markeren any
en de TypeScript compiler zal het type niet afleiden uit de toegewezen object waarde. Er zijn andere, betere manieren om dit te bereiken en die zullen we in dit artikel bespreken.
Interface Declaration
Hoewel de impliciete interface die we tot nu toe gezien hebben technisch gezien een type is, is het niet expliciet gedefinieerd. Zoals besproken, is een interface niets anders dan de vorm die een object kan aannemen. Als je een functie hebt die een argument accepteert dat een object zou moeten zijn, maar van een bepaalde vorm, dan moeten we dat argument (parameter) annoteren met een interfacetype.
In het bovenstaande voorbeeld hebben we een functie getPersonInfo
gedefinieerd die een objectargument accepteert dat firstName
, lastName
, age
en getSalary
velden van gespecificeerde gegevenstypen heeft. Merk op dat we een object dat namen van eigenschappen en hun overeenkomstige types bevat als type hebben gebruikt met behulp van de :<type>
annotatie. Dit is een voorbeeld van een anonieme interface, omdat de interface geen naam heeft, hij werd inline gebruikt.
Dit alles lijkt een beetje ingewikkeld om te verwerken. Als het ross
object ingewikkelder wordt en het moet op meerdere plaatsen gebruikt worden, dan lijkt TypeScript gewoon een ding dat je aanvankelijk leuk vond maar nu gewoon een lastig ding om mee om te gaan. Om dit probleem op te lossen definiëren we een interface type met behulp van interface
keyword.
In het bovenstaande voorbeeld hebben we een interface Person
gedefinieerd die de vorm van een object beschrijft, maar deze keer hebben we een naam die we kunnen gebruiken om naar dit type te verwijzen. Wij hebben dit type gebruikt om de variabele ross
te annoteren, evenals het argument person
van de functie getPersonIfo
. Dit zal TypeScript informeren om deze entiteiten te valideren tegen de vorm van Person
.
Waarom een interface gebruiken?
Interface type kan belangrijk zijn om een bepaalde vorm af te dwingen. Typisch in JavaScript, stellen we blind vertrouwen in runtime dat een object altijd een bepaalde eigenschap zal bevatten en dat die eigenschap altijd een waarde van een bepaald type zal hebben, zoals {age: 21, ...}
als voorbeeld.
Wanneer we daadwerkelijk beginnen met het uitvoeren van bewerkingen op die eigenschap zonder eerst te controleren of die eigenschap bestaat op het object of dat de waarde ervan is wat we verwachtten, kunnen dingen misgaan en het kan uw toepassing onbruikbaar achterlaten na afloop. Bijvoorbeeld, {age: '21', ...}
, hier age
waarde is een string
.
Interfaces bieden een veilig mechanisme om met dergelijke scenario’s om te gaan tijdens het compileren. Als je per ongeluk een eigenschap gebruikt op een object dat niet bestaat of de waarde van een eigenschap gebruikt in een illegale bewerking, zal de TypeScript compiler je programma niet compileren. Laten we eens een voorbeeld bekijken.
In het bovenstaande voorbeeld proberen we de name
eigenschap van het _student
argument te gebruiken binnen de printStudent
functie. Aangezien het _student
argument een type is van de Student
interface, geeft de TypeScript compiler een foutmelding tijdens het compileren omdat deze eigenschap niet bestaat in de Student
interface.
Ook 100 — _student.firstName
is geen geldige bewerking omdat de firstName
eigenschap een type is van string
en de laatste keer dat ik heb gekeken, kun je in JavaScript geen string
van een number
aftrekken (resulteert in NaN
).
In het bovenstaande voorbeeld hebben we de traditionele manier gebruikt om functietype te schrijven voor het getSalary
veld. U kunt echter ook functie syntax gebruiken zonder de body voor hetzelfde, die over het algemeen wordt gebruikt in interfaces.
interface Student {
firstName: string;
lastName: string;
age: number;
getSalary(base: number): number;
};
Optionele eigenschappen
Soms moet een object een eigenschap hebben die gegevens van een bepaald gegevenstype bevat, maar het is niet verplicht om die eigenschap op het object te hebben. Dit is vergelijkbaar met de optionele functieparameters die we in de vorige les hebben geleerd.
Dergelijke eigenschappen worden optionele eigenschappen genoemd. Een interface kan optionele eigenschappen bevatten en we gebruiken de annotatie ?:Type
om ze weer te geven, net als de optionele functieparameters.
In het bovenstaande voorbeeld heeft de interface Student
de eigenschap age
, die optioneel is. Als de age
eigenschap echter wordt opgegeven, moet deze een waarde hebben van het type number
.
In het geval van het ross
object, dat een type is van de Student
interface, hebben we geen waarde opgegeven voor de age
eigenschap die legaal is, maar in het geval van monica
hebben we de age
eigenschap wel opgegeven, maar de waarde is string
wat niet legaal is. Daarom geeft de TypeScript compiler een fout.
De fout lijkt misschien vreemd, maar het is eigenlijk logisch. Als de age
eigenschap niet bestaat op een object, zal de object.age
undefined
teruggeven wat een type is van undefined
. Als deze wel bestaat, dan moet de waarde van het type number
zijn.
Dus de waarde van de age
eigenschap kan ofwel van het type undefined
zijn of van het type number
dat in TypeScript wordt weergegeven met union syntax number | undefined
.
💡 We zullen type unions leren in een Type System les.
Echter, optionele eigenschappen leveren serieuze problemen op tijdens de uitvoering van het programma. Stel dat we de eigenschap age
gebruiken in een rekenkundige bewerking, maar de waarde ervan is undefined
. Dit is een ernstig probleem.
Maar het goede is dat de TypeScript compiler niet toestaat dat er illegale bewerkingen worden uitgevoerd op een optionele eigenschap, omdat de waarde undefined
kan zijn.
In het bovenstaande voorbeeld voeren we een rekenkundige bewerking uit op de eigenschap age
, wat illegaal is omdat de waarde van deze eigenschap in de runtime number
of undefined
kan zijn. Het uitvoeren van rekenkundige bewerkingen op undefined
resulteert in NaN
(geen getal).
💡 Voor bovenstaand programma moesten we echter de
--strictNullChecks
vlag opfalse
zetten, wat een TypeScript compiler vlag is. Als we deze optie wel instellen, compileert bovenstaand programma prima.
Om deze foutmelding of waarschuwing te vermijden, moeten we de TypeScript compiler expliciet vertellen dat deze eigenschap een type is van number
en niet van number
of undefined
. Hiervoor gebruiken we type assertion (AKA type conversie of typecasting).
In het bovenstaande programma hebben we (_student.age as number)
gebruikt dat het type van _student.age
converteert van number | undefined
naar number
. Dit is een manier om TypeScript compiler te vertellen, “Hey, dit is een getal”. Maar een betere manier om hiermee om te gaan zou zijn om in runtime ook te controleren of _student.age
undefined
is en dan de rekenkundige bewerking uit te voeren.
💡 We zullen leren over type assertions in een Type System les.
Functietype met behulp van een interface
Niet alleen de vorm van een gewoon object, maar een interface kan ook de signatuur van een functie beschrijven. In de vorige les hebben we type alias gebruikt om een functietype te beschrijven, maar interfaces kunnen dat ook.
interface InterfaceName {
(param: Type): Type;
}
De syntaxis om een interface als functietype te declareren is vergelijkbaar met de functiehandtekening zelf. Zoals je in het voorbeeld hierboven kunt zien, bevat de body van de interface de exacte signature van een anonieme functie, zonder de body natuurlijk. Hier doen parameternamen er niet toe.
In het bovenstaande voorbeeld hebben we IsSumOdd
interface gedefinieerd die een functietype definieert dat twee argumenten van het type number
accepteert en een boolean
waarde retourneert. Nu kunt u dit type gebruiken om een functie te beschrijven, omdat het IsSumOdd
-interfacetype gelijkwaardig is aan functietype (x: number, y: number) => boolean
.
Een interface met een anonieme method-handtekening beschrijft een functie. Maar een functie in het JavaScript rijk is ook een object, wat betekent dat je eigenschappen kunt toevoegen aan een functie waarde net als een object. Daarom is het volkomen legaal dat u willekeurige eigenschappen kunt definiëren op een interface van het type functie.
In het bovenstaande voorbeeld hebben we type
en calculate
eigenschappen toegevoegd op de interface IsSumOdd
die een functie beschrijft. Met behulp van de methode Object.assign
voegen we type
en calculate
eigenschappen samen met een functiewaarde.
Interfaces van het functietype kunnen nuttig zijn om constructorfuncties te beschrijven. Een constructor functie is vergelijkbaar met een klasse die tot taak heeft objecten (instanties) te maken. We hadden alleen constructor functies tot ES5 om een class
in JavaScript na te bootsen. Daarom compileert TypeScript klassen naar constructor-functies als je ES5
of lager als doel hebt.
💡 Als je meer wilt leren over constructor-functies, volg dan dit artikel.
Als we new
sleutelwoord voor anonieme functiehandtekening in de interface zetten, maakt dat de functie constructible. Dat betekent dat de functie alleen kan worden aangeroepen met het new
sleutelwoord om objecten te genereren en niet met een gewone functie-aanroep. Een voorbeeld van een constructor functie ziet er als volgt uit.
function Animal( _name ) {
this.name = _name;
}var dog = new Animal( 'Tommy' );
console.log( dog.name ); // Tommy
Gelukkig hoeven we niet met constructor functies te werken omdat TypeScript het class
sleutelwoord biedt om een klasse te maken die veel gemakkelijker is om mee te werken dan een constructor functie, geloof me. In feite is een class
diep in de kern een constructor functie in JavaScript. Probeer het onderstaande voorbeeld maar eens.
class Animal{
constructor( _name ) {
this.name = _name;
}
}console.log( typeof Animal ); // "function"
Een klasse en een constructor functie zijn een en hetzelfde ding. Het enige verschil is dat de class
ons een rijke OOP syntax geeft om mee te werken. Vandaar dat een interface van een constructorfunctietype een klasse vertegenwoordigt.
In het bovenstaande voorbeeld hebben we de klasse Animal
gedefinieerd met een constructorfunctie die een argument van het type string
accepteert. U kunt dit beschouwen als een constructorfunctie die een soortgelijke signatuur heeft als de Animal
constructor.
De AnimalInterface
definieert een constructorfunctie, aangezien het anonieme functie heeft voorafgegaan door het new
sleutelwoord. Dit betekent dat de Animal
klasse in aanmerking komt om een type van AnimalInterface
te zijn. Hier is AnimalInterface
interface type gelijkwaardig aan het functietype new (sound: string) => any
.
De createAnimal
functie accepteert ctor
argument van AnimalInterface
type, dus kunnen we Animal
klasse als argumentwaarde doorgeven. We zullen niet in staat zijn om getSound
methode handtekening van de Animal
klasse toe te voegen in AnimalInterface
en de reden wordt uitgelegd in de Klassen les.
Indexeerbare Types
Een indexeerbaar object is een object waarvan de eigenschappen kunnen worden benaderd met behulp van een index handtekening zoals obj
. Dit is de standaard manier om een array-element te benaderen, maar we kunnen dit ook doen voor het object.
var a = ;
var o = { one: 1, two: 2, three: 3 };console.log( a ); // 1
console.log( a ); // 1 (same as `a.one`)
Op sommige momenten kan uw object een willekeurig aantal eigenschappen hebben zonder een bepaalde vorm. In dat geval kunt u gewoon het object
type gebruiken. Dit object
-type definieert echter elke waarde die niet number
, string
, boolean
, symbol
, null
, of undefined
is, zoals besproken in de basis-typen-les.
Als we strikt moeten controleren of een waarde een gewoon JavaScript-object is, kunnen we een probleem hebben. Dit kan worden opgelost met behulp van een interface-type met een index-handtekening voor de naam van de eigenschap.
interface SimpleObject {
: any;
}
De SimpleObject
-interface definieert de vorm van een object met string
sleutels waarvan de waarden any
datatype kunnen zijn. Hier wordt de key
eigenschapsnaam slechts gebruikt als plaatshouder, aangezien deze tussen vierkante haken staat.
In het bovenstaande voorbeeld hebben we ross
en monica
object gedefinieerd van het type SimpleObject
interface. Aangezien deze objecten string
-sleutels en waarden van het gegevenstype any
bevatten, is dit volkomen legaal.
Als u in de war bent over de sleutel 1
in monica
, die een type is van number
, is dit legaal omdat object- of array-items in JavaScript kunnen worden geïndexeerd met number
– of string
-sleutels, zoals hieronder wordt weergegeven.
var o = { 0: 'Zero', '1': 'One' };
var a = ;console.log( o ); // Zero
console.log( o ); // One
console.log( a ); // One
console.log( a ); // One
Als we preciezer moeten zijn over het type sleutels en hun waarden, kunnen we dat zeker ook doen. We kunnen bijvoorbeeld een indexeerbaar interfacetype definiëren met sleutels van het type number
en waarden van het type number
als we dat willen.
💡 Het sleuteltype van een indexhandtekening moet of
string
ofnumber
zijn.
In het bovenstaande voorbeeld hebben we een LapTimes
-interface gedefinieerd die eigenschapsnamen van het type number
en waarden van het type number
kan bevatten. Deze interface kan een gegevensstructuur vertegenwoordigen die kan worden geïndexeerd met number
-sleutels, dus array ross
en objecten monica
en joey
zijn legaal.
Het object rachel
voldoet echter niet aan de vorm van LapTimes
, omdat sleutel one
een string
is en alleen kan worden benaderd met string
, zoals rachel
en niets anders. Daarom zal de TypeScript compiler een foutmelding geven zoals hierboven getoond.
Het is mogelijk om sommige eigenschappen verplicht en sommige optioneel te hebben in een indexeerbaar interface type. Dit kan heel handig zijn als we een object een bepaalde vorm moeten geven, maar het maakt echt niet uit als we extra en ongewenste eigenschappen in het object krijgen.
In het bovenstaande voorbeeld hebben we een interface LapTimes
gedefinieerd die eigenschap name
met string
waarde en optionele eigenschap age
met number
waarde moet bevatten. Een object van het type LapTimes
kan ook willekeurige eigenschappen hebben waarvan de sleutels number
moeten zijn en waarvan de waarden ook number
moeten zijn.
Het object ross
is een geldig LapTimes
object, ook al heeft het niet de age
eigenschap, omdat deze optioneel is. monica
heeft echter wel de age
eigenschap, maar de waarde is string
, zodat het niet voldoet aan de LapTimes
interface.
Het joey
object voldoet ook niet aan de LapTimes
interface, omdat het een gender
eigenschap heeft, die een type is van string
. Het rachel
object heeft geen name
eigenschap die vereist is in de LapTimes
interface.
💡 Er zijn enkele gotchas waar we op moeten letten bij het gebruik van indexeerbare types. Deze worden in deze documentatie vermeld.
Extending Interface
Zoals klassen kan een interface eigenschappen van andere interfaces overerven. Echter, in tegenstelling tot klassen in JavaScript, kan een interface erven van meerdere interfaces. We gebruiken het sleutelwoord extends
om een interface te erven.
Door een interface uit te breiden, krijgt de kind-interface alle eigenschappen van de ouder-interface. Dit is heel nuttig wanneer meerdere interfaces een gemeenschappelijke structuur hebben en we code-duplicatie willen vermijden door de gemeenschappelijke eigenschappen in een gemeenschappelijke interface onder te brengen die later kan worden geërfd.
In het bovenstaande voorbeeld hebben we een Student
interface gemaakt die eigenschappen erft van de Person
en Player
interface.
Multiple Interface Declarations
In de vorige sectie hebben we geleerd hoe een interface de eigenschappen van een andere interface kan erven. Dit werd gedaan met behulp van het extend
sleutelwoord. Wanneer interfaces met dezelfde naam echter binnen dezelfde module (bestand) worden gedeclareerd, voegt TypeScript hun eigenschappen samen zolang ze verschillende eigenschapsnamen hebben of hun conflicterende eigenschapstypes hetzelfde zijn.
Ook is het volkomen legaal dat een veld van een interface het type heeft van een andere interface. In het volgende voorbeeld heeft het info
-veld van de Student
-interface het type van Person
-interface.