# Les 4: TypeScript Fundamentals - Huiswerk ANTWOORDEN Deze antwoordsleutel toont de volledige conversie van JavaScript naar TypeScript voor het huiswerk van Les 4. Voor elk bestand wordt een interface, type definitions en de complete TypeScript-implementatie getoond. --- ## 1. users.ts - Gebruikersbeheer ### Origineel JavaScript (users.js) De originele JavaScript-versie bevat vijf functies voor gebruikersbeheer: - `createUser()`: Maakt een nieuwe gebruiker aan met unieke ID en timestamp - `findUserByEmail()`: Zoekt een gebruiker op emailadres - `updateUser()`: Past gebruikerseigenschappen aan - `filterActiveUsers()`: Filtert alleen actieve gebruikers - `deactivateUser()`: Deactiveert een gebruiker ### Complete TypeScript Conversie ```typescript // src/users.ts /** * User interface - Definieert de structuur van een gebruikersobject */ interface User { id: string; name: string; email: string; age: number; isActive: boolean; createdAt: Date; } /** * Maakt een nieuwe gebruiker aan * @param name - De naam van de gebruiker * @param email - Het emailadres van de gebruiker * @param age - De leeftijd van de gebruiker * @returns Een nieuw User-object */ function createUser(name: string, email: string, age: number): User { return { id: Math.random().toString(36).substr(2, 9), name, email, age, isActive: true, createdAt: new Date() }; } /** * Zoekt een gebruiker op emailadres * @param users - Array van gebruikers * @param email - Het emailadres om naar te zoeken * @returns De gevonden User, of null als niet gevonden */ function findUserByEmail(users: User[], email: string): User | null { return users.find(user => user.email === email) || null; } /** * Werkt gebruikerseigenschappen bij * @param user - De originele gebruiker * @param updates - Gedeeltelijke updates (Partial) * @returns De bijgewerkte User */ function updateUser(user: User, updates: Partial): User { return { ...user, ...updates }; } /** * Filtert alleen actieve gebruikers * @param users - Array van gebruikers * @returns Array van actieve gebruikers */ function filterActiveUsers(users: User[]): User[] { return users.filter(user => user.isActive); } /** * Deactiveert een gebruiker * @param user - De gebruiker om te deactiveren * @returns De gebruiker met isActive = false */ function deactivateUser(user: User): User { return { ...user, isActive: false }; } export { createUser, findUserByEmail, updateUser, filterActiveUsers, deactivateUser }; export type { User }; ``` ### Sleutelbeslissingen bij TypeScript Conversie 1. **User Interface**: Een interface is gedefinieerd die alle properties van een gebruiker typeert. Dit zorgt voor type-safety en autocomplete. 2. **Functie Parameters**: Alle parameters zijn voorzien van expliciete TypeScript-typen: - `name: string`, `email: string`, `age: number` voor `createUser()` - `users: User[]` voor arrays van gebruikers 3. **Return Types**: Alle functies hebben expliciete return types: - `createUser()` retourneert `User` - `findUserByEmail()` retourneert `User | null` (union type voor null-safety) - `filterActiveUsers()` en `updateUser()` retourneren `User[]` respectievelijk `User` 4. **Partial Type**: De `updateUser()` functie accepteert `Partial` omdat niet alle eigenschappen hoeven te worden bijgewerkt. Dit is een built-in TypeScript utility type. 5. **Export**: Zowel functies als het User type worden geëxporteerd voor hergebruik in andere bestanden. --- ## 2. products.ts - Productbeheer ### Origineel JavaScript (products.js) De JavaScript-versie bevat: - `CATEGORIES`: Constante array met geldige categorieën - `createProduct()`: Maakt een nieuw product aan met validatie - `applyDiscount()`: Pas een kortingspercentage toe - `getExpensiveProducts()`: Filtert producten op minimale prijs - `formatPrice()`: Formatteert een prijs met valutasymbool - `rateProduct()`: Voegt een rating toe (1-5) ### Complete TypeScript Conversie ```typescript // src/products.ts /** * Union type voor productcategorieën */ type Category = 'electronics' | 'clothing' | 'food' | 'books' | 'sports'; /** * Product interface - Definieert de structuur van een productobject */ interface Product { id: string; name: string; price: number; category: Category; description: string | null; inStock: boolean; rating: number | null; } /** * Geldige productcategorieën */ const CATEGORIES: Category[] = ['electronics', 'clothing', 'food', 'books', 'sports']; /** * Maakt een nieuw product aan * @param name - Productnaam * @param price - Productprijs * @param category - Productcategorie (moet in CATEGORIES zitten) * @param description - Optionele productbeschrijving * @returns Een nieuw Product-object * @throws Error als categorie ongeldig is */ function createProduct( name: string, price: number, category: Category, description?: string ): Product { if (!CATEGORIES.includes(category)) { throw new Error(`Invalid category: ${category}`); } return { id: Math.random().toString(36).substr(2, 9), name, price, category, description: description || null, inStock: true, rating: null }; } /** * Passe een kortingspercentage op een product toe * @param product - Het originele product * @param percentage - Kortingspercentage (0-100) * @returns Product met verlaagde prijs * @throws Error als percentage buiten bereik is */ function applyDiscount(product: Product, percentage: number): Product { if (percentage < 0 || percentage > 100) { throw new Error('Discount must be between 0 and 100'); } const discountedPrice = product.price * (1 - percentage / 100); return { ...product, price: Math.round(discountedPrice * 100) / 100 }; } /** * Filtert producten op minimale prijs * @param products - Array van producten * @param minPrice - Minimale prijs * @returns Array van producten met prijs >= minPrice */ function getExpensiveProducts(products: Product[], minPrice: number): Product[] { return products.filter(product => product.price >= minPrice); } /** * Formatteert een prijs met valutasymbool * @param price - Het prijsbedrag * @param currency - Valutacode ('EUR', 'USD', 'GBP') * @returns Geformateerde prijsstring (bijv. '€19.99') */ function formatPrice(price: number, currency: string): string { const currencySymbols: Record = { EUR: '€', USD: '$', GBP: '£' }; const symbol = currencySymbols[currency] || currency; return `${symbol}${price.toFixed(2)}`; } /** * Voegt een rating toe aan een product * @param product - Het originele product * @param rating - Rating waarde (1-5) * @returns Product met rating * @throws Error als rating buiten bereik is */ function rateProduct(product: Product, rating: number): Product { if (rating < 1 || rating > 5) { throw new Error('Rating must be between 1 and 5'); } return { ...product, rating }; } export { createProduct, applyDiscount, getExpensiveProducts, formatPrice, rateProduct, CATEGORIES }; export type { Product, Category }; ``` ### Sleutelbeslissingen bij TypeScript Conversie 1. **Category Union Type**: In plaats van een willekeurige string, gebruiken we een union type `type Category = 'electronics' | 'clothing' | 'food' | 'books' | 'sports'`. Dit voorkomt ongeldige categorieën en geeft betere autocomplete in IDE's. 2. **Product Interface**: Definieert alle eigenschappen met correcte typen: - `category: Category` in plaats van `string` (type-safe) - `description: string | null` en `rating: number | null` voor optionele waarden - `price: number` voor numerieke waarden 3. **CATEGORIES Type**: `const CATEGORIES: Category[]` zorgt ervoor dat de array alleen geldige categorieën bevat. 4. **Overloading Voorkomen**: De `createProduct()` functie accepteert `description?: string` (optionele parameter) in plaats van `description = null`. Dit is schoner en meer TypeScript-idiomatic. 5. **Record**: Voor `currencySymbols` gebruiken we `Record` in plaats van een gewone object, wat typezekerheid biedt. 6. **Validatie Types**: De functies valideren hun inputs en gooien TypeErrors met duidelijke berichten. --- ## 3. orders.ts - Orderbeheer ### Origineel JavaScript (orders.js) De JavaScript-versie bevat: - `createOrder()`: Maakt een nieuwe order aan - `calculateTotal()`: Berekent de totale prijs - `updateOrderStatus()`: Wijzigt orderstatus met validatie - `getOrdersByUser()`: Filtert orders per gebruiker - `getOrdersByStatus()`: Filtert orders per status ### Complete TypeScript Conversie ```typescript // src/orders.ts /** * Union type voor mogelijke orderstatussen */ type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled'; /** * OrderItem interface - Representeert een product in een order */ interface OrderItem { productId: string; name: string; price: number; } /** * Order interface - Definieert de structuur van een orderobject */ interface Order { id: string; userId: string; products: OrderItem[]; total: number; status: OrderStatus; createdAt: Date; } /** * Type voor geldige statustransities */ type ValidTransitions = Record; /** * Maakt een nieuwe order aan * @param user - Het User-object (moet minimaal id hebben) * @param products - Array van Product-objecten * @returns Een nieuw Order-object met status 'pending' */ function createOrder( user: { id: string }, products: Array<{ id: string; name: string; price: number }> ): Order { return { id: Math.random().toString(36).substr(2, 9), userId: user.id, products: products.map(p => ({ productId: p.id, name: p.name, price: p.price })), total: calculateTotal(products), status: 'pending', createdAt: new Date() }; } /** * Berekent het totaalbedrag van producten * @param products - Array van producten met price property * @returns Totaalbedrag */ function calculateTotal(products: Array<{ price: number }>): number { return products.reduce((sum, product) => sum + product.price, 0); } /** * Wijzigt de status van een order * @param order - De originele order * @param newStatus - De nieuwe status * @returns Order met bijgewerkte status * @throws Error als transactie ongeldig is */ function updateOrderStatus(order: Order, newStatus: OrderStatus): Order { const validTransitions: ValidTransitions = { pending: ['processing', 'cancelled'], processing: ['shipped', 'cancelled'], shipped: ['delivered'], delivered: [], cancelled: [] }; if (!validTransitions[order.status].includes(newStatus)) { throw new Error(`Cannot transition from ${order.status} to ${newStatus}`); } return { ...order, status: newStatus }; } /** * Filtert orders van een specifieke gebruiker * @param orders - Array van orders * @param userId - De gebruikers-ID * @returns Array van orders van die gebruiker */ function getOrdersByUser(orders: Order[], userId: string): Order[] { return orders.filter(order => order.userId === userId); } /** * Filtert orders op status * @param orders - Array van orders * @param status - De filterstatus * @returns Array van orders met die status */ function getOrdersByStatus(orders: Order[], status: OrderStatus): Order[] { return orders.filter(order => order.status === status); } export { createOrder, calculateTotal, updateOrderStatus, getOrdersByUser, getOrdersByStatus }; export type { Order, OrderStatus, OrderItem }; ``` ### Sleutelbeslissingen bij TypeScript Conversie 1. **OrderStatus Union Type**: `type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled'` zorgt ervoor dat alleen geldige statussen kunnen worden gebruikt. 2. **OrderItem Interface**: Gescheiden interface voor orderitems maakt de code modulair en leesbaar. 3. **Order Interface**: Gebruikt het `OrderStatus` type voor de status property in plaats van `string`. 4. **Flexible Parameter Types**: `createOrder()` accepteert `user: { id: string }` in plaats van volledige User-interface. Dit maakt de functie flexibel en ontkoppeld van usersbeheer. 5. **ValidTransitions Type**: `Record` type typeert de transities-object correct. Elke status mappen naar een array van geldige volgende statussen. 6. **Generieke Product Parameter**: `calculateTotal()` accepteert `Array<{ price: number }>` wat betekent dat het werkt met elk object dat een price-property heeft. 7. **Status Filtering**: `getOrdersByStatus()` accepteert `status: OrderStatus` waardoor alleen geldige statussen kunnen worden gefilterd. --- ## 4. utils.ts - Hulpfuncties ### Origineel JavaScript (utils.js) De JavaScript-versie bevat: - `formatDate()`: Formatteert een Date naar DD-MM-YYYY - `generateId()`: Genereert een willekeurige ID-string - `validateEmail()`: Valideert e-mailadressen met regex - `sortBy()`: Generieke functie om items te sorteren - `groupBy()`: Generieke functie om items te groeperen ### Complete TypeScript Conversie ```typescript // src/utils.ts /** * Formatteert een Date-object naar DD-MM-YYYY string * @param date - De Date-object * @returns Geformateerde datestring */ function formatDate(date: Date): string { const day = String(date.getDate()).padStart(2, '0'); const month = String(date.getMonth() + 1).padStart(2, '0'); const year = date.getFullYear(); return `${day}-${month}-${year}`; } /** * Genereert een willekeurige ID-string * @returns Unieke ID-string */ function generateId(): string { return Math.random().toString(36).substr(2, 9); } /** * Valideert of een string een geldig e-mailadres is * @param email - De email-string om te valideren * @returns true als geldig, false als ongeldig */ function validateEmail(email: string): boolean { const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return regex.test(email); } /** * Sorteert een array van items op een specifieke property * Generieke functie met type parameters * @param items - Array van items om te sorteren * @param key - De property-naam om op te sorteren * @param direction - Sorteerrichting 'asc' of 'desc' * @returns Gesorteerde array (original wordt niet gewijzigd) */ function sortBy>( items: T[], key: keyof T, direction: 'asc' | 'desc' ): T[] { return [...items].sort((a, b) => { const aValue = a[key]; const bValue = b[key]; if (direction === 'desc') { return aValue > bValue ? -1 : 1; } return aValue > bValue ? 1 : -1; }); } /** * Groepeert items in een object op basis van een property * Generieke functie met type parameters * @param items - Array van items om te groeperen * @param key - De property-naam om op te groeperen * @returns Object waar keys de groepwaarden zijn en values arrays van items */ function groupBy>( items: T[], key: keyof T ): Record { return items.reduce((groups, item) => { const value = item[key]; if (!groups[value]) { groups[value] = []; } groups[value].push(item); return groups; }, {} as Record); } export { formatDate, generateId, validateEmail, sortBy, groupBy }; ``` ### Sleutelbeslissingen bij TypeScript Conversie 1. **Generieke Type Parameters**: - `>` zorgt ervoor dat T elk object-type kan zijn met string-keys - Dit maakt `sortBy()` en `groupBy()` herbruikbaar voor alle object-arrays 2. **keyof T**: In plaats van een string voor de `key` parameter, gebruiken we `keyof T`. Dit ziet er in de IDE voor dat je alleen geldige properties van T kunt selecteren. 3. **Literal Union Type voor Direction**: `direction: 'asc' | 'desc'` in plaats van `string`, wat fouten voorkomt met ongeldige richtingen. 4. **Return Types**: - `sortBy()` retourneert `T[]` (dezelfde type als input) - `groupBy()` retourneert `Record` 5. **Spread Operator Preserving**: In `sortBy()` gebruiken we `[...items]` om een kopie te maken. TypeScript begrijpt dat dit een `T[]` retourneert. 6. **Type Casting**: In `groupBy()` gebruiken we `as Record` om TypeScript te helpen begrijpen dat we een lege Record initialiseren. 7. **Non-Mutating Approach**: Beide functies wijzigen de originele array niet, wat een best practice is voor pure functions. --- ## Samenvattend: TypeScript Conversie Principes ### 1. **Interfaces voor Data Structures** - Elk belangrijk data-object krijgt een interface (User, Product, Order) - Interfaces documenteren de verwachte structuur en verbeteren IDE support ### 2. **Union Types voor Enumeraties** - In plaats van willekeurige strings, gebruiken we union types: - `type Category = 'electronics' | 'clothing' | ...` - `type OrderStatus = 'pending' | 'processing' | ...` - Dit voorkomt fouten en verbetert type-safety ### 3. **Generieke Types** - Voor herbruikbare functies als `sortBy()` en `groupBy()` gebruiken we generieke parameters - `>` zorgt voor type-safety terwijl flexibiliteit behouden blijft ### 4. **Null/Undefined Handling** - Union types met null/undefined: `string | null`, `number | null` - Dit dwingt expliciete handling van lege waarden ### 5. **Parameter Type Hints** - Alle parameters hebben expliciete TypeScript-typen - Optionele parameters gebruiken `?` of `...?: Type` syntax ### 6. **Return Type Annotations** - Alle functies hebben expliciete return type annotations - Dit helpt compile-time fouten opsporen en documenteert intent ### 7. **keyof en Generieke Constraints** - `keyof T` zorgt ervoor dat je alleen geldige properties kunt selecteren - `extends Record` constrained het type naar objecten ### 8. **Typing van Object Methods** - `Partial` voor optionele updates - `Record` voor type-safe object literals - `Pick` en `Omit` voor subset typen --- ## Test Compatibiliteit Alle bovenstaande TypeScript conversies zijn ontworpen om perfect te werken met de bestaande test suite: - **users.test.ts**: Tests gebruiken `User` interface - **products.test.ts**: Tests gebruiken `Product` interface en `Category` type - **orders.test.ts**: Tests gebruiken `Order`, `OrderStatus` en gerelateerde typen - **utils.test.ts**: Tests gebruiken generieke functies met impliciete type-inferentie De TypeScript-versies behouden exact dezelfde logic en runtime-gedrag als de originele JavaScript-versies, terwijl ze de voordelen van type-safety bieden.