6. TypeScript
6. TypeScript
TypeScript is een superset van JavaScript. Dat betekent dat deze taal alles bevat wat ook in JavaScript aanwezig is, maar dat het daarbovenop heel wat extra features aanbiedt, de belangrijkste daarvan is static type checking.
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 types, 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. Aangezien die veeleer een opsomming is van de features die TypeScript biedt, is er voor deze les geen voorbeeldcode beschikbaar.
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.
Als je een TypeScript bestand wilt uitvoeren met Node, kan dat, op voorwaarde dat je een recente LTS-versie gebruikt. In dat geval geef je de --strip-experimental-types vlag mee aan het node commando. Merk op dat dit enkel werkt als de type-informatie zonder problemen verwijderd kan worden uit het bestand, dit is bijvoorbeeld niet het geval voor een enum.[2]
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 | undefined = undefined
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 opties.[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 .Record<string, number>is dus een object dat strings als sleutel heeft en een getal als bijhorende waarde.[3]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.never: Een type dat nooit voorkomt, dit is bijvoorbeeld het geval voor een functie die altijd een foutmelding geeft of een oneindige loop bevat.
Interfaces & type aliases
Bovenstaande types zijn zelden 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 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,
}Soms is een interface niet voldoende, bijvoorbeeld om een type te definiëren voor een array met een vast aantal elementen of voor union types die slechts enkele voorgedefinieerde string kunnen bevatten. 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'Bovenstaande Person interface kan ook gedefinieerd worden als een type alias, dit is volledig equivalent.
type Person = {
name: string
firstName: string
accomplishments: string[]
yearOfBirth: number
// Een optionele property.
yearOfDeath?: number
}Zowel interfaces als type aliases kunnen samengevoegd worden.
Begrip: Types samenvoegen
Zowel types als interfaces kunnen met elkaar gecombineerd worden, dit kan door een & toe te voegen tussen de types of interfaces die je wilt combineren.
interface Person {
name: string
firstName: string
accomplishments: string[]
yearOfBirth: number
// Een optionele property.
yearOfDeath?: number
}
interface Employee {
employeeId: number
department: string
}
type EmployeePerson = Person & EmployeePick & Omit
Soms is het nodig om een eigenschap uit een interface te halen, of juist een subset van de eigenschappen te behouden. In dat geval kunnen de utility types Pick en Omit gebruikt worden.
Begrip: Pick & Omit
Via de Pick utility kan een subset van de eigenschappen van een interface behouden worden. De eerste parameter van deze generische utility is de interface waarvan je een subset wilt behouden, de tweede parameter is een union type van strings die de namen van de eigenschappen bevatten die je wilt behouden.
interface Person {
name: string
firstName: string
accomplishments: string[]
yearOfBirth: number
// Een optionele property.
yearOfDeath?: number
}
type BasicInfo = Pick<Person, 'name' | 'firstName' | 'yearOfBirth'>Via de Omit utility kunnen bepaalde eigenschappen uit een interface verwijderd worden. De eerste parameter van deze generische utility is de interface waaruit je properties wilt verwijderen, de tweede parameter is een union type van strings die de namen van de eigenschappen bevatten
interface Person {
name: string
firstName: string
accomplishments: string[]
yearOfBirth: number
// Een optionele property.
yearOfDeath?: number
}
type PersonalInfo = Omit<Person, 'accomplishments' | 'yearOfDeath'>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 moet geen naam meegeven worden. 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
}Klassen
Alhoewel klassen zelden gebruikt worden in JavaScript, worden deze wel ondersteund in JavaScript. TypeScript voegt hier extra features aan toe zoals readonly properties, access modifiers (public, private, protected), abstracte klassen, ...
Begrip: Klassen
Klassen worden aangemaakt via het class keyword.
Via het implements keyword kunnen klassen aangeven dat ze een interface implementeren, dit betekent dat de klasse alle eigenschappen en methodes van de interface moet bevatten.
Via het extends keyword kunnen klassen aangeven dat ze een andere klasse uitbreiden, dit betekent dat de klasse alle eigenschappen en methodes van de andere klasse erft, en dat deze eventueel overschreven kunnen worden. Als de klasse waarvan overgeërft wordt een abstracte klasse is, dan moeten alle abstracte methodes van deze klasse geïmplementeerd worden in de concrete subklasse.
interface Person {
name: string
firstName: string
accomplishments: string[]
yearOfBirth: number
// Een optionele property.
yearOfDeath?: number
}
class Employee implements Person {
name: string
firstName: string
accomplishments: string[]
yearOfBirth: number
yearOfDeath?: number
employeeId: number
department: string
constructor(name: string, firstName: string, accomplishments: string[], yearOfBirth: number, employeeId: number, department: string) {
this.name = name
this.firstName = firstName
this.accomplishments = accomplishments
this.yearOfBirth = yearOfBirth
this.employeeId = employeeId
this.department = department
}
}abstract class Shape {
abstract area(): number
}
class Circle extends Shape {
radius: number
constructor(radius: number) {
super()
this.radius = radius
}
area(): number {
return Math.PI * this.radius ** 2
}
}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. ↩︎
Er zijn bepaalde TypeScript features die niet zomaar verwijderd kunnen worden, zoals enums, namespaces, ... Je kan deze features verbieden in je code door de erasableSyntaxOnly compiler optie in te schakelen. ↩︎
Maps hebben ongeveer dezelfde functionaliteit, maar hebben enkele voordelen, we verwijzen de geïnteresseerde lezer door naar MDN voor meer informatie. ↩︎