TypeScript

Sebastiaan Henaumei 17, 2023Ongeveer 6 minuten

TypeScript

Op deze pagina wordt een inleiding TypeScript gegeven. TypeScript wordt gebruikt doorheen alle drie de React vakken binnen de graduaatsopleiding programmeren van Thomas More Kempen. We verwachten dan deze leerstof gekend is en gebruikt wordt voor alle opdrachten, examens en oefeningen, deze leerstof zal echter niet rechtstreeks besproken worden tijdens de les omdat dit heel sterk lijkt op hoe types werken binnen talen zoals C# of Java en de overstap zou niet moeilijk mogen zijn.

In klassieke JavaScript is het relatief eenvoudig om spaghetticode te schrijven. Callback-hell is een veelvoorkomend probleem. Daarnaast is er ook geen ondersteuning voor datatypes, typechecking, access modifiers, return types, inheritance, ... Kortom, het is lastig om grote, onderhoudbare, applicaties te schrijven in JavaScript. Dit wil natuurlijk niet zeggen dat dit onmogelijk is, maar er bestaat een 'betere' manier. TypeScript is een superset van JavaScript, dat betekent dat deze taal alles bevat wat er in JavaScript aanwezig is, maar dat het daarbovenop heel wat extra features toevoegt.

TypScriptopen in new window code is strongly typed, dit betekent dat elke variabele, elke functie, ... types krijgt. Dus kan een IDE ook foutmeldingen geven als je, bijvoorbeeld, een string gebruikt terwijl de functie een integer verwacht. TypeScript biedt ook andere nuttige zaken zoals optionele variabelen, enums, inheritance, generics, ... 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 getranspileerd worden naar klassieke JavaScript code. Hiervoor wordt een transpiler zoals SWCopen in new window gebruikt.

Types

Klassieke JavaScript ondersteunt geen static type checking, dit betekent dat onderstaande code geldig is.

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.

Type 'string' is not assignable to type 'number' (2322)

Je kan dit uittesten in de TypeScript Playgroundopen in new window.

In dit geval ziet de code er hetzelfde uit voor TypeScript en JavaScript, we kunnen in TypeScript echter ook uitdrukkelijk aangeven wat het type is van een bepaalde variabele.

let i: number = 10

In dit geval heeft het weinig nut om het type uitdrukkelijk mee te geven, TypeScript kan dit namelijk afleiden omdat we 10 (een number) als waarde hebben meegegeven. Als de variabele niet meteen geïnitialiseerd wordt is het wel nodig om het type mee te geven.

let i: number
i = 10

TypeScript ondersteund, onder anderen, volgende types:

Zelf gedefinieerde types

Bovenstaande types zijn natuurlijk niet altijd voldoende, het is regelmatig nodig om zelf een type (object) te bouwen. Hiervoor kunnen we klassen, interfaces, of enums definiëren. Stel we willen een User object, dat de voornaam, achternaam, en leeftijd van een gebruiker bevat.

interface User {
    firstName: string,
    lastName: string,
    age: number,
}

We kunnen bovenstaand type dan gebruiken om aan te geven dat een variable een User object bevat.

const lector: User = {
    firstName: 'Sebastiaan',
    lastName: 'Henau',
}

Bovenstaande code genereert de foutmelding

Property 'age' is missing in type '{ firstName: string lastName: string }' but required in type ' User'.

We kunnen het datatype aanpassen zodat de leeftijd optioneel wordt:

interface User {
    firstName: string
    lastName: string
    age?: number
}

const lector: User = {
    firstName: 'Sebastiaan',
    lastName: 'Henau',
}



 






De code genereert nu geen foutmeldingen meer, de leeftijd mag leeg blijven. In tegenstelling tot de variabele i in het eerste voorbeeld is het hier wel nodig om aan te geven dat de variabele lector van het type User is. Als we dit niet meegeven zal TypeScript het type van de variabele lector afleiden als any en zal er dus geen typechecking zijn.

Enums

Stel je hebt een heel gelimiteerd aantal mogelijke waarden, bijvoorbeeld de kleuren rood, groen en blauw. Je kan dit op een aantal manieren noteren in TypeScript. De eerste optie is een string variabele met slechts deze drie mogelijke waarden.

let color: 'red' | 'green' | 'blue'

Alhoewel dit werkt, is dit niet bijzonder mooi of gebruiksvriendelijk. Een iets beter oplossing is om via de type operator een type Color te definiëren. Zo kunnen we het nieuwe type op meerdere plaatsen gebruiken.

type Color = 'red' | 'green' | 'blue'
let color: Color

Binnen objectgeoriënteerde programmeertalen wordt in dit geval een enum (enumerated type) gebruikt, we kopiëren dit in TypeSctipt.

enum Color {
    red,
    green,
    blue,
}

console.log(Color.red === 0)

Standaard krijgt elk element in het enum een nummer, beginnende bij 0. Je kan dit uittesten via bovenstaande playground link. Het is echter ook mogelijk om de waarden binnen het enum een string waarde te geven, dit is vooral handig als je over de opties wil itereren om ze te tonen in een dropdown menu of iets soortgelijks.

enum Color {
    red = 'r',
    green = 'g',
    blue = 'b'
}

console.log(Color.red === 'r')

Functies

De parameters van een functie krijgen, net als variabelen, een type. De notatie is net hetzelfde. We kunnen opnieuw zowel de basis types of zelfgemaakte types gebruiken. Merk op dat functies ook een return type moeten krijgen, als de functie niets teruggeeft kan void gebruikt worden. Hieronder zie je twee alternatieve schrijfwijzen, gebruik voor dit vak bij voorkeur de tweede.

function deleteUser(user: User): void {
    // Delete the user
}

const deleteUser = (user: User): void => {
    // Delete the user
}

Indien je de signatuur van een functie wil definiëren, bijvoorbeeld als property van een interface, dan kan dit op een gelijkaardige manier als in het tweede voorbeeld hierboven. We kunnen echter ook opnieuw gebruik maken van de type operator.

interface StringUtils {
    camelCaseToSnakeCase: (camelCase: string) => string
    snakeCaseToCamelCase: (snakeCase: string) => string
    toLocaleCapitalizedString: (old: string)  => string
}

Optional chaining

Optionele variabelen beteken dat je constant moet controleren of de variabele al dan niet defined is. Als je er van uit gaat dat een optionele variabele steeds aanwezig is, is het onvermijdelijk dat je ergens een is undefined error zal tegenkomen. Dit willen we natuurlijk vermijden. Onderstaande interface heeft een optionele array die hobby's bevat.

Stel we willen de eerste hobby uitprinten en houden geen rekening met de optionele aard van de array hobbies.

interface User {
  firstName: string
  lastName: string
  hobbies?: string[]
}

const lector: User = {
  firstName: 'Sebastiaan',
  lastName: 'Henau',
}

console.log(lector.hobbies[0])

Bovenstaande code geeft de error:

lector.hobbies is possibly undefined

We kunnen dit natuurlijk eenvoudig oplossen, via een if-then blok.

// Beide schrijfwijzen zijn identiek.
// if (lector.hobbies !== undefined) {
if (lector.hobbies) {
    console.log(lector.hobbies[0])
}
 




Een if-then blok lost het probleem op, er zijn geen foutmeldingen meer en de code produceert nooit errors. Maar dit is wel relatief omslachtig, een if voor elke optionele waarden maakt je code onnodig lang. JavaScript biedt een betere oplossing, de optional chaining (?.) operator kan gebruikt worden om een if te vervangen.

console.log(lector.hobbies?.at(0))

Merk op dat we nu de methode at gebruikt hebben om het array-element op te halen. De optional-chaining parameter werkt enkele met methodes. Let op, de at methode is pas beschikbaar vanaf ES2022.

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 weer een if-then-else gebruiken om te controleren of de variabele null of undefined is, maar dit is opnieuw onnodig lang. De nullish coalescing operator biedt een oplossing. Deze operator geeft het rechter element terug als het linker element null of undefined is.

console.log(null ?? 'Dit wordt teruggegeven')
console.log(undefined ?? 'Dit wordt teruggegeven')
console.log('' ?? 'Dit wordt NIET teruggegeven')

Logical OR vs Nullish coalescing operator

Bovenstaande code gaf voor het laatste statement het linkerlid terug. Als je elke falsy waarde als ongeldig wil beschouwen, dan kan je in de plaats van de nullish coalescing operator gebruik maken van de logische of (||).

console.log('' || 'Dit wordt teruggegeven')
console.log('Dit wordt teruggeven' || 'Dit wordt NIET teruggegeven')

Importeren en exporteren

De meeste TypeScript applicaties zullen groot zijn en over verschillende bestanden verspreid staan. Het is dus nodig code te importeren en te exporteren. Stel, we hebben een map met twee bestanden, foo.ts en bar.ts.

src
|--foo.ts
|--bar.ts

De inhoud van bar.ts is als volgt.

export class Bar {
    // Some code
}

export const zeroList = Array(10).fill(0)

const zeroList2 = Array(10).fill(0)
export default zerolist2 // Maximum 1 default export per file.

Zowel de klasse Bar als de variabele zeroList worden geëxporteerd en kunnen geïmporteerd worden in foo.ts

import { Bar } from './bar'            // Importeer enkel de klasse Bar.
import { Bar, zeroList } from './bar'  // Importeer zowel de klasse Bar als de variabele zeroList.
import { zeroList } from './bar'       // Importeer enkel de variabele zeroList. 
import zeroList2 from './bar'          // Geen accolades nodig omdat we de default export gebruikt hebben.