6. TypeScript
6. TypeScript
TypeScript is een superset van JavaScript die typechecking toevoegt. Dat betekent dat deze taal alles bevat wat ook in JavaScript aanwezig is, maar dat het daarbovenop heel wat extra features toevoegt.
TypScript code is strongly typed, dit betekent dat elke variabele, elke functie, ... types krijgt. De IDE of editor kan zo foutmeldingen geven als je, bijvoorbeeld, een string gebruikt terwijl de functie een integer verwacht. TypeScript biedt ook andere nuttige zaken zoals optionele variabelen, enums, generics, utility type, overloading, ... Features die ons in staat stellen om onderhoudbare, leesbare, en herbruikbare code te schrijven.
Natuurlijk is er geen enkele browser die TypeScript ondersteunt, daarom moet TypeScript code steeds geconverteerd worden naar klassieke JavaScript code (door alle type informatie weg te halen). Hiervoor wordt een compiler zoals tsc, SWC, esbuild of babel gebruikt[1].
Hieronder bespreken de algemene TypeScript syntax en passen deze vervolgens toe om een eenvoudige library management app te bouwen.
Info
Om de algemene syntax te testen, kan je gebruik maken van een bun script met de .ts extensie. Bun kan naast JavaScript ook TypeScript code uitvoeren.
Built-in types
Klassieke JavaScript ondersteunt geen static type checking, dit betekent dat onderstaande code geldig is.
let i = 10
i = '10'Alhoewel er niets verkeerd is met talen zonder static type checking, is het veel minder eenvoudig om fouten te maken in talen waar er wel aan type checking gedaan wordt. Als we bovenstaande code schrijven in een TypeScript project krijgen we onderstaande foutmelding te zien, de code compileert niet.
Merk op dat we niet expliciet hebben moeten aangeven dat de variabele i een getal bevat, TypeScript leidt dit automatisch af van de waarde die toegekend is aan de variabele. We kunnen het type echter ook expliciet definiëren, al heeft dit hier niet echt zin.
let i: number = 10Als de variabele niet meteen geïnitialiseerd wordt is het daarentegen wel nodig om het type mee te geven.
let i: number
i = 10Begrip: TypeScript types
TypeScript ondersteund, onder anderen, volgende types:
number: Een getal, zowel kommagetallen als integers worden gedefinieerd door dit type. TypeScript maakt geen onderscheid tussen een integer, float of double.string: Een tekstwaarde.boolean: True of false.x[]: Een array van type , waar één van de andere types in deze lijst is.x[][]: Een tweedimensionale array van type , kan uitgebreid worden naar meer dimensies.(x | y): Een union type, waarden van type en worden allebei geaccepteerd, kan uitgebreid worden naar meerdere types.[x, y, ..., z]: Een tuple met een vast aantal elementen die elk een vast type hebben, hier zijn , en dus types uit deze lijst.Record<T, K>: Een object met keys van het type die verwijzen naar informatie van het type , een eenvoudig object dus.Record<string, number>is dus een object dat strings als sleutel heeft en een getal als bijhorende waarde.[2]any: Eender welk type, gebruik dit zo min mogelijk.unknown: Een veiligere versie vanany, alles kan toegekend worden aan eenunknownvariabele, maar je kan verder niets doen met deze variabele tenzij je expliciet controleert of de variabele een bepaald type heeft.
Interfaces
Bovenstaande types zijn niet altijd voldoende, het is regelmatig nodig om zelf een type te declareren, denk bijvoorbeeld aan een applicatie waarmee een boekenverzameling beheerd kan worden. Voor elke boek moet specifieke data bewaard worden, een interface kan deze datastructuur beschrijven.
Begrip: Interfaces
Een interface is een manier om de vorm van een object te definiëren in TypeScript. Een interface specificeert welke eigenschappen (en soms methodes) een object moet hebben, zonder dat het bepaalt hoe deze geïmplementeerd worden.
In tegenstelling tot objectgeoriënteerde talen zoals C# en Java worden interfaces enkel gebruikt in combinatie met klassen. In TypeScript is dit niet het geval, een interface wordt doorgaans gebruikt om data in een variabele te beschrijven en wordt zelden gebruikt om het contract van een klasse vast te leggen.
Eens een interface gedefinieerd is, kan deze gebruikt worden als elk ander TypeScript type. Het is dus mogelijk om union types, arrays, ... van interfaces te definiëren.
interface Person {
name: string
firstName: string
accomplishments: string[]
yearOfBirth: number
// Een optionele property.
yearOfDeath?: number
}
// Door hier : Person toe te voegen, garanderen we
// dat de correcte properties in het object zitten.
// In dit geval kan TypeScript het type niet afleiden en
// moet dit expliciet aangegeven worden.
const alanTuring: Person = {
name: 'Turing',
firstName: 'Alan',
accomplishments: [
'Turing machines',
'Turing test',
'Enigma codebreaker',
],
yearOfBirth: 1912,
yearOfDeath: 1954,
}Type aliases
Soms is een interface niet voldoende, bijvoorbeeld om een type te definiëren voor een array met een vast aantal elementen. In dat geval kan een type alias een oplossing bieden.
Begrip: Type alias
Meestal kan een variabele beschreven worden met primitief type of via een interface, soms is het echter nodig om iets te beschrijven dat niet in een interface past.
Denk hierbij aan coördinaten in een twee-dimensionale ruimte, we weten dat dit steeds een paar is, en dat er twee getallen bewaard moeten worden. We zouden hiervoor onderstaande interface kunnen gebruiken.
interface Coordinate {
xCoordinate: number
yCoordinate: number
}Alhoewel dit werkt, is het redelijk omslachtig, zeker als we dit type zouden willen uitbreiden naar meer dimensies. Via een type alias kan dit herschreven worden als
type Coordinate = [number, number]Ook in andere situaties kan een type alias nuttig zijn, bijvoorbeeld als je een string variabele hebt die slechts een beperkt aantal waardes kan aannemen.
type Position = 'left' | 'right' | 'top' | 'bottom'Functies
Aangezien de parameters van functies niet meegegeven worden tijdens de definitie van de functie, maar pas tijdens het oproepen van de functie, kan TypeScript de types niet afleiden en moeten deze altijd meegegeven worden. Hiervoor wordt dezelfde notatie gebruikt als bij variabelen, i.e. een dubbelpunt gevolgd door het type.
Naast parameters heeft een functie ook een return type, hiervoor gebruiken we opnieuw een dubbelpunt, dit keer achter de parameterlijst. Als de functie niets teruggeeft, wordt dit aangeduid met void of Promise<void>> voor asynchrone functies.
function deleteUserSync(user: User): void {
// Delete the user synchronously
}
async function deleteUser(user: User): Promise<void> {
// Delete the user asynchronously
}
const deleteUserSync = (user: User): void => {
// Delete the user synchronously
}
const deleteUser = async (user: User): Promise<void> => {
// Delete the user asynchronously
}Indien je de signatuur van een functie wil definiëren, bijvoorbeeld als property van een interface, dan kan dit op een gelijkaardige manier als bij de arrow functies hierboven. Het dubbelpunt moet vervangen worden door een pijl en we moeten geen naam meegeven. Onderstaande interface verwacht dus drie properties die elk een functie als waarde hebben, deze functie krijgt een string binnen en geeft een string terug.
interface StringUtils {
camelCaseToSnakeCase: (camelCase: string) => string
snakeCaseToCamelCase: (snakeCase: string) => string
toLocaleCapitalizedString: (old: string) => string
}Optional chaining
Een optionele property in een object betekent dat we telkens moeten controleren of de variabele al dan niet gedefinieerd is voordat deze gebruikt wordt. Doen we dit niet, dan is het onvermijdelijk dat we ergens een is undefined error tegenkomen. Via de optional chaining operator is dit eenvoudig te doen.
Begrip: Optional chaining
De optional chaining operator (?.) controleert of het linkerlid gedefinieerd is, voordat het rechterlid verder geëvalueerd wordt. Via deze operator kunnen TypeErrors voorkomen worden zonder dat hiervoor uitgebreide if-else constructies geschreven moeten worden.
interface Profile {
firstName: string
lastName: string
hobbies?: string[]
}
const alan: Profile = {
firstName: 'Alan',
lastName: 'Turing',
}
// De eerste hobby uitloggen zonder optional chaining.
if (alan.hobbies) {
console.log(alan.hobbies.at(0))
}
// De eerste hobby uitloggen met optional chaining
console.log(alan.hobbies?.at(0))Nullish coalescing operator
Het is regelmatig nodig om een default waarde mee te geven aan een variabele, bijvoorbeeld als we de instellingen van een gebruiker willen inlezen en merken dat deze nog niet bestaan (eerste keer dat de gebruiker de applicatie gebruikt). We kunnen natuurlijk een if-then-else gebruiken om te controleren of de variabele null of undefined is, maar dit is opnieuw onnodig.
De nullish coalescing operator biedt een oplossing.
Begrip: Nullish coalescing operator
De nullish coalescing operator is een logische operator die het rechterlid teruggeeft als het linkerlid null of undefined is, en anders het linkerlid.
console.log(null ?? 'Dit wordt teruggegeven')
console.log(undefined ?? 'Dit wordt teruggegeven')
console.log('' ?? 'Dit wordt NIET teruggegeven')Dit is slechts een beperkte subset van de beschikbare compilers, daarnaast zijn sommige van de vermelde tools tot meer in staat dan enkel het transpilen van TypeScript naar JavaScript. ↩︎
Maps hebben ongeveer dezelfde functionaliteit, maar hebben enkele voordelen, we verwijzen de geïnteresseerde lezer door naar MDN voor meer informatie; ↩︎