Les 1: Inleiding & TypeScript
Les 1: Inleiding & TypeScript
Tijdens deze inleiding worden de verschillen tussen de meest populaire tools voor mobile development besproken. Daarnaast bespreken we hoe we TypeScript kunnen gebruiken om types en extra foutcontroles toe te voegen aan een JavaScript applicatie.
Mobile development
Toen smartphones relatief nieuw waren, was het nodig om een app specifiek te ontwikkelen voor een bepaald platform (iOS, Android, Windows Phone, BlackBerry OS, ...). Elk besturingssysteem gebruikte hun eigen programmeertaal, zo werd Java gebruikt voor Android en Objective-C voor iOS. Vandaag de dag zijn er nog maar twee relevante platformen, Android en iOS. De programmeertalen die gebruikt worden op deze twee platformen zijn ook geëvolueerd. Voor Android kan naast Java nu ook Kotlin (een superset van Java) gebruikt worden, voor iOS gebruik je Swift in de plaats van Objective-C.
De programmeertalen zijn geëvolueerd, maar om native apps te ontwikkelen moet je nog steeds twee talen kennen en twee aparte codebases onderhouden. Dit is natuurlijk niet ideaal. Twee codebases betekent twee keer zoveel werk. Daarbovenop is de kans groot dat iemand die gespecialiseerd is in Java weinig tot geen kennis heeft van Swift en omgekeerd. Hoogstwaarschijnlijk zijn er dus verschillende programmeurs nodig voor beide versies van de app.
Naast deze nadelen zijn er natuurlijk ook voordelen verbonden aan het gebruikt van Swift, Java en Kotlin. De nieuwste SDK (Software Development Kit) kan altijd gebruikt worden, je hebt toegang tot de nieuwste features zodra Google of Apple deze vrijgeven. Daarbovenop is een app geschreven in Java of Swift performanter dan de alternatieven. In de meeste gevallen is het verschil nauwelijks of niet merkbaar, maar voor grafisch intensieve applicaties of andere zware applicaties bouw je best een aparte applicatie per platform. Tenslotte is het voor native applicaties ook eenvoudiger om correct te integreren met de UI van je gekozen platform.
Hybrid-webview & Hybrid-native
Twee codebases onderhouden is niet altijd praktisch of nodig. Er zijn verschillende tools beschikbaar waarmee mobiele applicaties, in één taal, geschreven kunnen worden voor zowel iOS als Android. We spreken in dat geval van hybrid applicaties. Hybrid apps kunnen geschreven worden in JavaScript, C#, Dart, Python, Kotlin ... Je kan dus de taal kiezen waar je als ontwikkelaar het bekendst mee bent. Maar er zijn natuurlijk ook nadelen verbonden aan dit soort applicaties. Je bent afhankelijk van een derde partij, je gebruikt frameworks en plug-ins die niet altijd up-to-date zijn en meestal niet de allerlaatste features van Android of iOS kunnen gebruiken.
Er zijn twee soorten hybrid apps te onderscheiden. De eerste soort worden verpakt in een iOS en Android app die niets anders doet dan een website laden in een webview, een browser met andere woorden. De tweede soort applicaties worden geschreven in één taal naar keuze, maar wordt gecompileerd naar native Java of Swift code. Dit soort apps zijn (bijna) even performant als echte native apps.
React Native
React Native is een React framework om hybrid native apps te schrijven. React Native projecten worden geschreven in JavaScript en maken gebruik van een 100% gedeelde codebase. React Native bied React UI-componenten aan die vertaald worden naar native iOS of Android componenten. Als je het opleidingsonderdeel 'JavaScrip Framework React' gevolgd hebt, is de overstap naar React Native zeer klein.
Flutter
Flutter is een relatief nieuw framework ontwikkeld door Google. Flutter apps worden geschreven in Dart, één codebase kan gecompileerd worden naar een mobiele, web of desktop (Linux, Windows, macOS) applicatie. Dart is ontwikkeld met het transpileren naar verschillende platformen in het achterhoofd en biedt bijzonder goede performance op de verschillende platformen.
Ionic
Ionic is een hybrid webview framework. Ionic applicaties worden dus niet gecompileerd naar native code. Toch biedt Ionic UI-componenten aan die zich automatisch aanpassen aan het platform waarop de applicatie uitgevoerd wordt. Ionic apps kunnen ontwikkeld worden in Angular, React, Vue of klassieke JavaScript. Aan deze laatste opties zijn er wel wat nadelen verbonden. Via Capacitor wordt de applicatie geconverteerd tot een hybrid-webview applicatie die, voor de gebruiker, niet te onderscheiden is van een native app. Ionic apps kunnen eenvoudig gepubliceerd worden als progressive web apps of desktop applicaties via Electron.
.NET MAUI
De .NET Multi-platform App UI of MAUI is een recent (november 2021) uitgebrachte tool voor multi-platform development. Via MAUI kan met één C# codebase code geschreven worden voor Android, iOS, Windows, macOS. MAUI is de opvolger van Xamarin.Forms en Xamarin.Native, maar combineert de beste aspecten van beide. Een MAUI UI-component wordt automatisch getranspileerd naar een gepaste native component op het doelplatform. In de voorganger kon je één UI bouwen voor iOS en Android, maar dit werd in een webview geladen. Daarbovenop bied MAUI APIs om platform-specifieke APIs zoals sensors, netwerk, klembord, filesystem, ... aan te spreken. Hiervoor was in Xamarin.Native steeds Java of Swift code nodig.
Progressive Web Apps
Progressive Web Apps (PWAs) zijn webapplicaties waaraan service workers en een web manifest aan toegevoegd worden. Deze technologieën zorgen ervoor dat de applicatie vanuit een browser geïnstalleerd kan worden op een smartphone of desktop computer. De service workers bieden offline functionaliteit. Gegevens worden in een cache bewaard, als de applicatie offline gebruikt wordt, zal de data uit de cache geladen worden. Indien het toestel met het internet verbonden is zal de applicatie draaien als een website en dus automatisch de laatste versie downloaden.
PWAs bieden, net zoals browsers, ondersteuning voor zaken zoals notifications, geo-location, camera, ... Maar, niet alle functionaliteit die in een hybrid app gebruikt kan worden, is beschikbaar voor PWAs. Een volledig overzicht (per browser) is te vinden op https://whatwebcando.today/.
PWAs kunnen geïnstalleerd worden vanuit een browser, of vanuit de App Store op macOS toestellen en de Windows Store op Windows 11 toestellen. De meest gebruikte mobiele browsers bieden eveneens ondersteuning voor PWAs, op desktop computers is dit echter niet altijd het geval. Enkel Chromium gebaseerde browsers en Safari bieden ondersteuning voor PWAs op desktop computers, Firefox heeft besloten om deze functionaliteit niet langer te ondersteunen. Via een extensie als Progressive Web Apps for Firefox kan je de mogelijkheid om PWAs te installeren wel toevoegen aan Firefox.
Onderstaande video demonstreert hoe de PWA-versie van de cursuswebsite offline geïnstalleerd kan worden via Google Chrome op een desktopcomputer. Als je dit zelf wil proberen, weet dat het even kan duren voordat de installatieknop verschijnt. De PWA is geconfigureerd om deze optie pas weer te geven wanneer alle video's en afbeeldingen lokaal gecachet zijn. Aangezien deze website zeer veel video's bevat, kan dit even duren.
TypeScript
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 features toevoegt.
TypScript 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 Babel gebruikt.
Types
Klassieke JavaScript ondersteund 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 de foutmelding
Type 'string' is not assignable to type 'number' (2322)
te zien. Je kan dit uittesten in de TypeScript Playground.
In dit geval ziet de code er hetzelfde uit voor TypeScript en JavaScript, we kunnen in TypeScript echter ook expliciet aangeven wat het type is van een bepaalde variabele.
let i: number = 10;
In dit geval heeft het weinig nut om het type expliciet 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 volgende types:
number: Een getal, zowel een kommagetal als een integer is hier geldig. TypeScript maakt geen onderscheid tussen een integer, float of double.stringany: Eender welk type, probeer dit zoveel mogelijk te vermijden.booleanx[]: Een array van type x, waar x één van de andere types in deze lijst is.x[][]: Een tweedimensionale array van type x, kan uitgebreid worden naar meerdere dimensies.(x | y): Type x of type y, kan uitgebreid worden naar meerdere types.[x, y, ..., z]: Een tuple met een vast aantal elementen die elk een vast type hebben.
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 flexibel. Binnen objectgeoriënteerde programmeertalen wordt in dit geval een enum, of enumerated type, gebruikt.
enum Color {
red,
green,
blue
}
console.log(Color.red === 0);
Standaard krijgt elk element in het enum een nummer, beginnende bij . 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 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 eerste.
function deleteUser(user: User): void {
// Delete the user
}
const deleteUser = (user: User): void => {
// Delete the user
}
Optional chaining
Optionele variabelen gaan gekoppeld met errors, 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.
interface User {
firstName: string,
lastName: string,
hobbies?: string[]
}
const lector: User = {
firstName: "Sebastiaan",
lastName: "Henau"
}
Stel we willen de eerste hobby uitprinten en houden geen rekening met de optionele aard van de array hobbies.
console.log(lector.hobbies[0]);
Bovenstaande code geeft de error:
Object is possibly undefined
We kunnen dit natuurlijk eenvoudig oplossen, via een if-then blok.
if (lector.hobbies !== undefined) {
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 bied een betere oplossing, de ?. 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 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 app start). 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 ?? 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);
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.