6. TypeScript
6. TypeScript
Oefening 0: Voorbereiding
Voordat je een TypeScript project start, heb je een aantal dingen nodig. De TypeScript code moet getranspileert worden naar JavaScript voordat deze door Node of de browser uitgevoerd kan worden. Daarnaast moet TypeScript geïnstalleerd worden voordat je IDE of editor de nodige type-informatie heeft voor autocompletion en foutmeldingen.
Vanaf volgende les gebruiken we templates waar het merendeel van de nodige tools al in zit, deze les voeren we de (eenvoudiger) configuratie manueel uit zodat je een beter idee krijgt van wat de commando's van volgende week precies doen.
Maak een nieuw project aan via pnpm init en installeer TypeScript als dev dependency via pnpm add typescript -D. Intalleer ook het @types/node package zodat je de types van Node kan gebruiken in je project, dit is nodig voor één van functies die je hieronder zal gebruiken. Je voert deze commando's best uit in een nieuwe map, anders kan dit tot onoverzichtelijke code lijken.
Nu dat TypeScript geïnstalleerd is, moet dit nog geconfigureerd worden. Dit doe je door een tsconfig.json bestand aan te maken via onderstaand commando.
Via tsconfig.json weet de TypeScript compiler (en de IDE/editor) welke bestanden getranspileerd moeten worden en waar de type-informatie te vinden is. Hiervoor gebruik je respectievelijk de include en types properties. Daarnaast wordt ook de gebruikte ECMAScript versie ingesteld, en de module resolutie (voor het importeren van bestanden).
Voeg de src map en de index.ts file toe aan de include property. Voeg node toe aan de types array zodat je ook de types van Node kan gebruiken. Zet de lib property op esnext zodat je de nieuwste JavaScript features kan gebruiken.
{
"compilerOptions": {
"module": "nodenext",
"target": "esnext",
"types": ["node"],
"sourceMap": true,
"declaration": true,
"declarationMap": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"strict": true,
"jsx": "react-jsx",
"verbatimModuleSyntax": true,
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"skipLibCheck": true
},
"include": ["index.ts", "src/**/*"]
}Tenslotte moet het type veld in package.json ingesteld worden op module zodat je ES modules (import/export) kan gebruiken in je code. Daarnaast moet het ingangspunt van de applicatie ook ingesteld worden, dit doe je via de main property. Maak hiervoor ook al een index.ts file aan in de root van je project.
{
"name": "typescript-lecture",
"version": "1.0.0",
"type": "module",
"main": "index.js",
"devDependencies": {
"@types/node": "^20.11.1",
"typescript": "^5.2.2"
}
}Als optionele uitbreiding kan je ook een linter en formatter installeren, zoals beschreven in de appendix. Als je dit doet, moet je de noAlert en noConsole regels uitzetten, aangezien je hier met een terminal applicatie werkt, heb je deze functies nodig. Daarnaast moet ook de vcs.useIgnoreFile optie in biome.json op false gezet worden, anders zal Biome niet werken omdat er geen .gitignore bestand is.
Oefening 1: Game character manager
Om de code via Bun uit te voeren, gebruik je het volgende commando (ervan uitgaand dat je index.ts als ingangspunt gebruikt hebt):
bun index.ts1.1 Interface en filter
Het doel is om een app te maken om karakters van spellen in aan te maken, en te kunnen filteren op een stat.
Maak daarvoor eerst een interface aan voor je karakter die volgende properties heeft:
id(string uuid)name(string)class(string)health(int)mana(int)lastStrike(datum + tijd of null)
Maak deze interface aan in een aparte file.
Vul zelf een array met enkele characters in code (gebruik eventueel AI). Wanneer de app start vertrek je van deze array. Maak deze array aan in een aparte file. Kies voor korte en simpele namen (maakt het testen makkelijker) en vermijd dubbele namen. De waarde van health zet je op een getal tussen 1 en 15 en de waarde van mana tussen 1 en 5.
Als het programma start vraag je eerst aan de gebruiker op welke stat deze wil filteren (class, health, mana).
Filter on which stat? (class, health, mana):Als de gebruiker 'class' heeft opgegeven, vraag je welke class en zoek je op die class (niet hoofdlettergevoelig).
Als de gebruiker health of mana heeft opgegeven vraag je wat de minimumwaarde van de stat moet zijn.
Je filtert de array op basis van deze 2 vragen en toont de gebruiker het resultaat. Het resultaat toont alle data van het karakter op een propere manier weergegeven.
Filter on which stat? (class, health, mana): class
Which class? mage
--- Characters ---
ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Name: Luna
Class: Mage
Health: 8
Mana: 5
Last Strike: 2/6/2026, 1:00:00 AMFilter on which stat? (class, health, mana): health
Minimum value for health? 4
--- Characters ---
ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Name: Luna
Class: Mage
Health: 8
Mana: 5
Last Strike: 2/6/2026, 1:00:00 AM
ID: b2c3d4e5-f6a7-8901-bcde-f12345678901
...Filter on which stat? (class, health, mana): mana
Minimum value for mana? 5
--- Characters ---
ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Name: Luna
Class: Mage
Health: 8
Mana: 5
Last Strike: 2/6/2026, 1:00:00 AM
ID: f6a7b8c9-d0e1-2345-fabc-456789012345
...1.2 Menu en toevoegen
Vertrek van jouw oplossing voor oefening 1.
Bouw een klein menu in je app waar voorlopig volgende opties in zitten:
===== GAME MENU =====
1: Filter characters
2: Add characterAls de gebruiker 1 kiest, doet de app wat die in oefening 1 deed.
Als de gebruiker 2 kiest, dan vraag je de gebruiker achter alle properties behalve de id en de lastStrike. De id genereer je, en de lastStrike zet je op null.
Op basis van deze antwoorden maak je een nieuw karakter aan en voeg je dit toe aan de array. Daarna toon je de volledige array.
Om een karakter aan te maken, heb je een nieuw UUID nodig, hiervoor kan je de crypto module van Node gebruiken.
import {randomUUID} from 'node:crypto'
const id = randomUUID()Your choice: 2
--- Add New Character ---
Name: Demo
Class: Mage
Health (1-15): 14
Mana (1-5): 3
--- Characters ---
ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Name: Luna
Class: Mage
Health: 8
Mana: 5
Last Strike: 2/6/2026, 1:00:00 AM
ID: b2c3d4e5-f6a7-8901-bcde-f12345678901
...1.3: Lus, exit en refractor
Neem het resultaat van oefening 2, en bouw een lus rond je oefening en een extra optie 'exit' in je menu. De lus herhaalt telkens het maken van de keuze en de gevolgen van de keuze, en sluit af als de gebruiker 'exit' kiest.
Je menu ziet er nu zo uit:
===== GAME MENU =====
1: Filter characters
2: Add character
0: ExitSplits je code op in meerdere functies en gebruik typescript om deze op te bouwen. Je hebt op zijn minst functies die ongeveer het volgende doen:
- Toon menu
- Verwerk keuze
- Filter
- Voeg toe
- Toon karakters
Bij voorkeur splits je ook deze methodes nog logisch op.
1.4: Attack
Neem het resultaat van oefening 3, en bouw een extra menuoptie 'Attack' met nummer 3. Als de gebruiker die optie kiest, toont de applicatie eerst alle karakters en hun info. Herbruik hiervoor de methode om de karakters te tonen.
Vraag aan de gebruiker welk karakter de aanval uitvoert. De gebruiker geeft de naam van het karakter in, deze controle mag niet hoofdlettergevoelig zijn. Je maakt hiervoor een methode die de naam binnen krijgt en het volledige object of null teruggeeft op basis van de array. Als de naam niet gevonden is, toon je gewoon het hoofdmenu opnieuw. Dit karakter is je aanvaller.
Doe hetzelfde, maar vraag nu welk karakter aangevallen moet worden. Herbruik de methode die je net hebt aangemaakt. Als je gebruiker 2 keer hetzelfde karakter kiest, dan valt die gewoon zichzelf aan. Je mag als uitbreiding op de oefening een filter maken en op dubbels controleren, maar dit is niet noodzakelijk. Dit karakter is je verdediger.
Genereer nu een random getal tussen 1 en 10 om te kijken hoeveel schade je zal aanrichten. Check eerst of de aanvaller wel voldoende 'mana' over heeft. Elke aanval verbruikt 2 mana punten. Indien de aanvaller minder dan 2 mana punten heeft, toon je aan de gebruiker dat het karakter te weinig mana heeft.
Vervolgens pas je de aanval toe:
- Het mana van de aanvaller moet 2 punten naar beneden
- De health van de verdediger moet verminderd worden met de schade die je berekend hebt
- De lastStrike van de aanvaller moet ingesteld worden op de huidige datum en tijd
- Als de health van de verdediger 0 of minder is, dan toon je dat deze dood is en verwijder je deze uit de array
Maak 1 of meerdere methodes aan om de aanval te programmeren.
--- Attack ---
--- Characters ---
ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Name: Luna
Class: Mage
Health: 8
Mana: 5
Last Strike: 2/6/2026, 1:00:00 AM
...
Who is attacking? (name): luna
Who is being attacked? (name): mort
Mort has died!--- Attack ---
--- Characters ---
ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Name: Luna
Class: Mage
Health: 8
Mana: 5
Last Strike: 2/6/2026, 1:00:00 AM
...
Who is attacking? (name): Luna
Who is being attacked? (name):
Character "" not found.1.5: Type aliases
Neem het resultaat van oefening 4, en vervang het type van de class property door een type alias waar je (minstens) 5 classes in zet. Voor inspiratie kan je classes nemen uit boardgames, video games, of science fiction. Verzin gerust zelf grappige classes.
Maak nog een type alias aan om de aanvaller en verdediger samen in te bewaren in een tuple. Je krijgt nu een type 'attack' waar je eerst de aanvaller en dan de verdediger in bewaart.
Pas de type aliases ook toe in je bestaande code.
1.6: Herhaling classes
We hebben het hieronder over het begrip 'class' in het programmeren en niet over de class van je karakter.
Maak een class GameManager aan en herschrijf je app zodat deze nu object geörienteerd is. In de constructor geef je de array van karakters mee. De code in je index.ts zal dus enkel nog de Game klasse aanmaken, de array meegeven, en een methode start() oproepen die de app start.
1.7: Optionele uitbreidingen
- Voeg een stat damage toe die als multiplier dient voor je aanval
- Voeg een stat defense toe die als multiplier dient voor je aanval (=> vermenigvuldig met 1 gedeeld door je defense stat)
- Voeg een functie heal toe die een karakter zen health vermeerdert met 10 en mana met 4
- Voeg een type alias toe die de status van je karakter kan bijhouden (dead/alive) en voeg die toe als property van de karakter. Pas de status mee aan bij elke aanval en toon deze wanneer je de karakters toont.
- Sla de karakters op in een JSON-bestand dat je uitleest in plaats van een array te gebruiken. Voor een voorbeeld van de nodige methodes, kan je de voorbeeldcode van les 5 raadplegen.
- Maak een optie 'opslaan' die het JSON-bestand dat je uitleest update
Optioneel & uitdagend 2: Tic-Tac-Toe vs. AI
Tijdens deze oefening bouwen we een fundament voor een programma waarmee een 2-speler bordspel zoals, Tic-Tac-Toe, dammen, of schaken gespeeld kan worden tegen een AI. In de oefeningreeks voegen we enkel Tic-Tac-Toe toe, maar de code die je schrijft, is generiek zodat je er later makkelijk een ander spel aan kan toevoegen. Om het AI te bouwen gebruiken we het minimax algoritme.
Deze oefening is optioneel, uitdagend en complex. Hierdoor is het niet mogelijk om je code tussentijds te testen, de volledige oefening moet afgewerkt worden voordat je iets kunt testen.
Gebruik doorheen deze oefening zoveel mogelijk types, interfaces, discriminated unions en type aliases. De opgave beschrijft niet waar je deze nodig hebt, probeer dit zelf te identificeren.
2.1 GamePiece klasse
Schrijf een abstracte klasse GamePiece die één spelstuk (pion, toren, X, O, ...) voorstelt. Deze klasse heeft volgende eigenschappen en methodes:
player: Een readonly instantievariabele die bijhoudt welke speler dit stuk bezit. Er zijn twee opties, Computer of Player.constructor: Een constructor die de speler als parameter krijgt en deze toewijst aan de instantievariabele.toString: Een abstracte methode die een stringrepresentatie van het stuk teruggeeft.
2.2 BoardGame klasse
Schrijf een abstracte klasse BoardGame die een spelbord voorstelt. Deze klasse heeft volgende eigenschappen en methodes:
board: Een readonly 2D array van GamePiece of null objecten.state: Een instantievariabele die de huidige status van het spel bijhoudt. Er zijn drie opties, InProgress, Win, of Draw.constructor: Een protected constructor die de grootte van het bord als parameter krijgt en het bord initialiseert met null waarden.size: Een getter die de grootte van het spelbord teruggeeft.applyMove: Een abstracte methode die een zet toepast op het bord en geen returnwaarde heeft. De functie heeft twee parameters, een zet en het bord waarop de zet toegepast moet worden. Het bord is optioneel. Een zet bestaat uit drie stukken informatie, de optionele positie vanwaar de zet vertrekt (voor spellen waar je een stuk moet verplaatsen), de positie waar de zet eindigt, en het stuk dat verplaatst wordt.readMove: Een abstracte methode die de gebruiker vraagt om een zet in te voeren en deze zet teruggeeft.isValidMove: Een abstracte methode die controleert of een zet geldig is en een boolean teruggeeft. De zet wordt doorgegeven als parameter.getAvailableMoves: Een abstracte methode die een array van mogelijke zetten teruggeeft op basis van twee parameters, het bord waarop de zetten toegepast moeten worden, en de speler voor wie de zetten gegenereerd moeten worden.getWinner: Een methode die controleert of er een winnaar is en deze teruggeeft als deze er is, anders wordt undefined teruggegeven. De methode heeft één optionele parameter, het bord dat gecontroleerd moet worden.renderBoard: Een concrete methode die het bord op een nette manier in de console toont. De uitvoer hieronder toont een voorbeeld van een Tic-Tac-Toe bord. Merk op dat elk vak even breed is, ongeacht of er een stuk in staat of niet.
| 1 | 2 | 3 |
|---|---|---|
1 | X | X | O |
|---|---|---|
2 | X | X | |
|---|---|---|
3 | O | O | |
|---|---|---|2.3 MiniMax klasse
Schrijf een abstracte klasse MiniMax die het minimax algoritme implementeert.
Het minimax algoritme werkt door een boom van mogelijke zetten te genereren, waarbij elke laag van de boom een beurt voorstelt. Als de gewenste diepte bereikt is, of als er een winnaar is of een gelijkspel, dan wordt de score van dat bord berekend.
In onderstaand diagram wordt gespeeld door de computer en door de speler. Voor elk van de leaf-nodes (de onderste laag) wordt de score berekend, de borden waar wint krijgen een positieve score, de borden waar wint krijgen een negatieve score, en de borden waar niemand wint krijgen een score van 0. De zet die tot de hoogste score leidt, is de zet die de computer zal spelen.

game: Een protected readonly instantievariabele die een BoardGame object bijhoudt.maxDepth: Een protected readonly instatievariabele die het maximale aantal lagen in de minimax boom bijhoudt.constructor: Een protected constructor die een BoardGame object en een maximum diepte als parameters krijgt en deze toewijst aan de instantievariabelen.evaluateBoard: Een abstracte methode die een bord als parameter krijgt en de score van dat bord teruggeeft.minimax: Een concrete private methode die een bord, de huidige diepte, en een boolean die aangeeft of de huidige speler de maximizer is, als parameters krijgt. De code voor deze methode wordt hieronder gegeven, maar je kan deze ook zelf proberen te implementeren op basis van onderstaand stappenplan.Bouw een array van alle beschikbare zetten.
Controleer of de diepte 0 bereikt is en of er geen beschikbare zetten meer zijn. Als één van de twee voorwaarden voldaan is, geef je een object terug met de berekende score van het bord.
Bouw een object met een score van -Infinity als de huidige speler de maximizer (computer) is, of Infinity als de huidige speler de minimizer (gebruiker) is. De opgegeven scores zijn de slechts mogelijke voor zowel de maximizer als de minimizer (uit het oogpunt dat de pc moet winnen).
Itereer over alle beschikbare zetten, en pas de zet toe op het bord. Bereken vervolgens de score van deze zet door een recursieve call naar minimax te maken, waarbij de diepte met 1 verlaagd wordt, en de boolean die aangeeft of het om de maximizing speler gaat omgewisseld wordt. Bereken vervolgens de beste score door de huidige score te vergelijken met de score van deze zet en op basis van de isMaximizingPlayer boolean, de beste score te kiezen.
Als de beste score verandert door deze zet, sla dan ook deze zet op in het result object. Deze heb je immers nodig om te weten welke zet de computer moet spelen.
Let op, bij het toepassen van de zetten moet je ervoor zorgen dan je nog steeds terug kunt gaan naar de vorige staat van het bord, anders werkt het algoritme niet correct.
Code van de minimax methode
export interface MiniMaxResult { score: number move?: Move } #minimax(board: GameBoard, depth: number, isMaximizingPlayer: boolean): MiniMaxResult { const moves = this.game.getAvailableMoves(isMaximizingPlayer ? 'Computer' : 'Player', board) if (depth === 0 || moves.length === 0) { return {score: this.evaluateBoard(board)} } const fn = isMaximizingPlayer ? Math.max : Math.min const result: MiniMaxResult = { score: isMaximizingPlayer ? -Infinity : Infinity, } for (const move of moves) { const nextBoard = board.map(row => [...row]) this.game.applyMove(move, nextBoard) const newScore = fn(result.score, this.#minimax(nextBoard, depth - 1, !isMaximizingPlayer).score) if (result.score !== newScore) { result.score = newScore result.move = move } } return result }
play: Een concrete publieke methode die de een spel start. Deze methode vraagt de gebruiker om hij/zij al dan niet als eerste aan zet wil zijn.Zolang er geen winnaar is, of gelijkspel, wordt de console geleegd, het bord gerenderd. Vervolgens wordt afhankelijk van wie er aan zet is ofwel de zet van de gebruiker ingelezen en toegepast, ofwel de minimax methode aangeroepen om de beste zet te berekenen en toe te passen.
Als er een winnaar is, of gelijkspel, wordt dit geprint in de console en stopt het spel.
2.4 Concrete GamePieces
Schrijf twee nieuwe concrete klassen X en O die de GamePiece klasse implementeren voor de X en O speler.
2.5 TicTacToe klasse
Schrijf een nieuwe concrete klasse TicTacToe die de BoardGame klasse implementeert.
2.6 TicTacToeMiniMax klasse
Schrijf een nieuwe concrete klasse TicTacToeMiniMax die de MiniMax klasse implementeert. De evaluateBoard methode geeft 10 terug als de maximizing speler (computer) wint, -10 als de minimizing speler (gebruiker) wint, en 0 als er niemand wint.
2.7 TicTacToe spelen
Schrijf een menu dat de gebruiker laat kiezen tussen de beschikbare spellen (in dit geval enkel TicTacToe) en vervolgens het gekozen spel start.
2.8 Algemeen menu
Bouw tenslotte nog een algemeen menu waar de gebruiker een oefening kan kiezen (1 of 2) en op basis van deze keuze, de code van de gekozen oefening wordt uitgevoerd.
Available exercises:
┌───┬────────────┐
│ │ Values │
├───┼────────────┤
│ 0 │ Exercise 1 │
│ 1 │ Exercise 2 │
└───┴────────────┘
Select an exercise by typing its index: