# Working with TypeScript ## 3.1 How to work with native classes and types related to the Window object in the browser? ### IntersectionObserver ```typescript const observer = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) { console.log('Element is in view!'); } }); }); observer.observe(document.querySelector('#someElement')); //Error: Argument of type 'Element | null' is not assignable to parameter of type 'Element'. Type 'null' is not assignable to type 'Element'. //solution: const observer: IntersectionObserver = new IntersectionObserver((entries: IntersectionObserverEntry[]) => { entries.forEach((entry: IntersectionObserverEntry) => { if (entry.isIntersecting) { console.log('Element is in view!'); } }); }); observer.observe(document.querySelector('#someElement')!); ``` ### Local Storage ```typescript const userSettings = localStorage.getItem('userSettings'); console.log(JSON.parse(userSettings)); //error: Argument of type 'string | null' is not assignable to parameter of type 'string'. Type 'null' is not assignable to type 'string' //solution const userSettings = localStorage.getItem('userSettings'); if (userSettings !== null) { console.log(JSON.parse(userSettings)); // Safely parsing, knowing it's not null } else { console.log("No user settings found."); } ``` ### Posibly return null value when select element ```typescript const myCanvas = document.querySelector('#myCanvas'); console.log(myCanvas.getContext('2d')); //Error: 'myCanvas' is possibly 'null' //=> can't call the function getContext() //solution: const myCanvas = document.querySelector('#myCanvas') as HTMLCanvasElement; console.log(myCanvas.getContext('2d')); // TypeScript knows myCanvas is a canvas ``` ### DOM manipulation #### Traversing the DOM ```htmlembedded <div> <ul id="main"> <li>hello</li> <li>world</li> </ul> </div> ``` ```typescript // Selecting an element const myList = document.getElementById('main') as HTMLUListElement; // Accessing children const listItems = myList.children; // HTMLCollection console.log("listItems", listItems) // Accessing the first child const firstItem = myList.firstElementChild as HTMLLIElement; console.log("firstItem", firstItem) // Navigating to siblings const secondItem = firstItem.nextElementSibling as HTMLLIElement; console.log("secondItem", secondItem) // Accessing the parent node const listParent = secondItem.parentElement as HTMLElement; console.log("listParent", listParent) ``` #### Creating and Appending Elements ```typescript // Creating a new list item const newItem = document.createElement('li') as HTMLLIElement; newItem.textContent = 'New Item'; // Appending the new item to the list myList.appendChild(newItem); ``` Example: create dynamic list ```typescript const addItemToList = (itemText: string): void =>{ const list = document.getElementById('main') as HTMLUListElement; const newItem = document.createElement('li') as HTMLLIElement; newItem.textContent = itemText; list.appendChild(newItem); } // Example usage addItemToList('Learn TypeScript'); ``` ## 3.2 How to assign dynamic values to the Window object? ### 3.2.1 Direct Extension Using declare global ```typescript // In a TypeScript module (.ts file) declare global { interface Window { customMethod: () => void; } } // Implementing the custom method window.customMethod = () => console.log("This is a custom method."); // Using the custom method window.customMethod(); ``` ### 3.2.2 Extending Window with a Custom Interface ```typescript // Defining a custom interface that extends the original Window interface interface CustomWindow extends Window { userToken?: string; // Example of a custom property } // Casting the global window object to the type of your custom interface declare let window: CustomWindow; // Setting a dynamic value window.userToken = 'abc123'; // Accessing the custom property console.log(window.userToken); // Outputs: abc123 ``` ### More example ```typescript // Method 1: Direct Extension Using declare global // Use Case: Custom Error Logging // Implementing a global error logging mechanism that can capture and report errors across the entire application. // // Extending the window object for global error logging declare global { interface Window { logError: (error: Error, context?: string) => void; } } // Implementation of the global error logging function window.logError = (error, context = "General") => { console.log(`Error in ${context}:`, error); // Here, you'd typically send the error information to a monitoring service }; // Global error handler window.onerror = (message, source, lineno, colno, error) => { window.logError(error || new Error(message.toString()), "Unhandled Exception"); return true; // Prevents the default handling of the error }; // Example of using the logging function directly try { // Some error-prone operation throw new Error('Something went wrong'); } catch (error) { window.logError(error, "Example Function"); } //Method 2: Extending Window with a Custom Interface // Use Case: Global State Management // In applications that don't use state management libraries or frameworks, it can be useful to attach a global state object to the window for easy access across different parts of the application, especially for small to medium-sized project // Defining an interface for the global state interface CustomWindow extends Window { appState: { userLoggedIn: boolean; theme: 'light' | 'dark'; setTheme: (theme: 'light' | 'dark') => void; }; } // Casting the global window object to the type of the custom interface declare let window: CustomWindow; // Initializing the global state window.appState = { userLoggedIn: false, theme: 'light', setTheme: function(theme: 'light' | 'dark') { this.theme = theme; // Additional logic to apply the theme across the application console.log(`Theme set to ${theme}`); }, }; // Example usage window.appState.setTheme('dark'); console.log(`User logged in: ${window.appState.userLoggedIn}`); ``` ## 3.3 How to go through the DOM objects using TypeScript ### 3.3.1. Type Assertions Common Elements Requiring Type Assertions - Input Elements (HTMLInputElement): For accessing value, checked. - Select Elements (HTMLSelectElement): To work with options, selectedIndex. - TextArea Elements (HTMLTextAreaElement): To manipulate value. - Button Elements (HTMLButtonElement): When accessing button-specific attributes. Recognizing When to Use Type Assertions - The element has properties or methods not present on the generic HTMLElement. - You're accessing form fields or media elements with specific properties (like value for inputs or play for audio/video). ```typescript //example 1: const myInput = document.getElementById("myInput"); console.log(myInput.value); // Error: Property 'value' does not exist on type 'HTMLElement | null'. //solution const myInput = document.getElementById("myInput") as HTMLInputElement; console.log(myInput.value); // Correctly accesses 'value'. //example 2: const myAudio = document.getElementById("myAudio"); myAudio.play(); // Error: Property 'play' does not exist on type 'HTMLElement | null'. //solution const myAudio = document.getElementById("myAudio") as HTMLAudioElement; myAudio.play(); // Correctly accesses 'play'. ``` ### 3.3.2. Runtime Type Checking TypeScript can't guarantee the actual type of an element at runtime. Therefore, checking an element's type before performing operations is crucial for avoiding errors. ```typescript //example 1: const myCanvas = document.getElementById("myCanvas"); const ctx = myCanvas.getContext("2d"); // Error: 'getContext' does not exist on type 'HTMLElement | null'. //solution const myCanvas = document.getElementById("myCanvas"); if (myCanvas instanceof HTMLCanvasElement) { const ctx = myCanvas.getContext("2d"); // Safe to use 'getContext' } //example 2: const myVideo = document.getElementById("myVideo"); myVideo.play(); // Error: 'play' does not exist on type 'HTMLElement | null'. //solution const myVideo = document.getElementById("myVideo"); if (myVideo instanceof HTMLVideoElement) { myVideo.play(); // Safe to use 'play' } ``` ### 3.3.3. Handling null and undefinedDOM ```typescript //example 1: const myDiv = document.getElementById("myDiv"); myDiv.textContent = "Hello World"; // Error: Object is possibly 'null'. //solution const myDiv = document.getElementById("myDiv"); if (myDiv) { myDiv.textContent = "Hello World"; // Safely updates the text. } //or const myDiv = document.getElementById("myDiv"); myDiv?.textContent = "Hello World"; // Safely updates the text if myDiv is not null. //example 2: const myButton = document.getElementById("myButton"); myButton.classList.add("active"); // Error: Object is possibly 'null'. //solution const myButton = document.getElementById("myButton"); if (myButton) { myButton.classList.add("active"); // Safely adds the class. } //or const myButton = document.getElementById("myButton"); myButton?.classList.add("active"); // Safely adds the class if myButton is not null. ``` ### 3.3.4. Generics in Utility Functions Creating utility functions for DOM manipulation without losing type information can be tricky, leading to type assertion repetition or errors. ```typescript //example 1: //Utility Function Without Generics: const getElement = (id: string) => { return document.getElementById(id); // Returns 'HTMLElement | null'. } getElement("myInput").value; // Error: Property 'value' does not exist. //solution const getElement<T extends HTMLElement> = (id: string): T | null => { return document.getElementById(id) as T | null; } const myInput = getElement<HTMLInputElement>("myInput"); console.log(myInput?.value); // Correct usage with type safety. //example 2: const getElementsByClassName = (className: string) => { return document.getElementsByClassName(className); // Returns 'HTMLCollectionOf<Element>'. } getElementsByClassName("myClass")[0].value; // Error: Property 'value' does not exist on type 'Element'. //solution const getElementsByClassName = <T extends Element>(className: string): HTMLCollectionOf<T> => { return document.getElementsByClassName(className) as HTMLCollectionOf<T>; } const myInputs = getElementsByClassName<HTMLInputElement>("myInputClass"); console.log(myInputs[0]?.value); // Correct usage with type safety. ``` ## 3.4 How to add event handlers to elements in the DOM using TypeScript ### 3.4.1. Basic Event Listening 1. Directly Adding Event Listeners Example: Adding a click event to a button. ```typescript const button = document.getElementById("myButton") as HTMLButtonElement; button.addEventListener("click", (event) => { console.log("Button clicked!"); }); ``` Type-Safe Event Handler: ```typescript button.addEventListener("click", (event: MouseEvent) => { console.log(event.clientX, event.clientY); // Access MouseEvent properties with type safety }); ``` 2. Using `this` in Event Handlers Logging the id of a button when it's clicked. ```typescript button.addEventListener("click", function(this: HTMLButtonElement) { this.classList.toggle('active'); console.log(`Button ${this.id} state: ${this.classList.contains('active') ? 'Active' : 'Inactive'}`); }); ``` 2. Dealing with Different Event Types 2.1 Handling Keyboard Events Example: Adding a keypress event to an input field. ```typescript const input = document.getElementById("myInput") as HTMLInputElement; input.addEventListener("keypress", (event: KeyboardEvent) => { console.log(`Key pressed: ${event.key}`); }); ``` 2.2 Managing Form Submit Events Example: Preventing a form from submitting. ```typescript const form = document.getElementById("myForm") as HTMLFormElement; form.addEventListener("submit", (event: SubmitEvent) => { const inputElement = document.getElementById('myInput') as HTMLInputElement; if (!inputElement.value.match(/^[a-zA-Z]+$/)) { event.preventDefault(); // Prevent form submission console.log('Form submission prevented due to validation failure.'); alert('Please enter only letters.'); } else { console.log('Form submitted successfully.'); } }); ``` ### 3.4.3. Advanced Event Handling Strategies **Event Delegation** Instead of adding an event listener to each element, add a single listener to a parent element and use event delegation. ```typescript //Example: Handling clicks on multiple buttons within a container. const container = document.getElementById("buttonContainer") as HTMLElement; container.addEventListener("click", (event: MouseEvent) => { const target = event.target as HTMLElement; if (target.tagName === "BUTTON") { console.log(`Button ${target.id} clicked`); } }); ``` ## 3.5 Using fetch method with TypeScript Generic solution ```typescript function api<T>(url: string, options?: RequestInit): Promise<T> { return fetch(url) .then(response => { if (!response.ok) { throw new Error(response.statusText); } return response.json() as Promise<{ data: T }>; }) .then(data => data.data) .catch((error: Error) => { console.error(error); // Log the error or use an external logging service throw error; // Rethrow for further handling }); } ``` Example 1: Fetching user information ```typescript interface User { id: number; name: string; email: string; } // Fetching user data api<User>('/api/users/1') .then(user => { console.log(user.name, user.email); }) .catch(error => { console.error("Failed to fetch user data:", error); }); ``` Example 2: Posting Data to an API ```typescript interface Post { userId: number; title: string; body: string; } //1. Function to post data async function createPost(postData: Post): Promise<Post> { return api<Post>('/api/posts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(postData), }); } // Usage const newPost: Post = { userId: 1, title: 'Hello TypeScript', body: 'TypeScript with Fetch API is awesome!', }; createPost(newPost) .then(post => console.log("Post created:", post)) .catch(error => console.error("Failed to create post:", error)); //2. Update post async function updatePost(postId: number, postData: Post): Promise<Post> { return api<Post>(`https://jsonplaceholder.typicode.com/posts/${postId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(postData), }); } // Usage const updatedPost: Post = { userId: 1, title: 'Updated Title', body: 'Updated body content.', }; updatePost(1, updatedPost) .then(post => console.log("Post updated:", post)) .catch(error => console.error("Failed to update post:", error)); //3. Delete post async function deletePost(postId: number): Promise<void> { return api<void>(`https://jsonplaceholder.typicode.com/posts/${postId}`, { method: 'DELETE', }); } // Usage deletePost(1) .then(() => console.log("Post deleted successfully.")) .catch(error => console.error("Failed to delete post:", error)); ``` ## 3.6 Async await and Promises with TypeScript Promise ```typescript //create a promise const myPromise: Promise<string> = new Promise((resolve, reject) => { const isSuccess = true; // Simulated outcome isSuccess ? resolve("Operation successful") : reject("Operation failed"); }); //handle promise using .then and .catch myPromise .then((result: string) => console.log(result)) // Success path .catch((error: string) => console.error(error)); // Error path ``` Using async - await ```typescript // Create a promise const myPromise: Promise<string> = new Promise((resolve, reject) => { const isSuccess = true; // Simulated outcome isSuccess ? resolve("Operation successful") : reject("Operation failed"); }); // Function to handle the promise using async - await const handleMyPromise = async () => { try { const result: string = await myPromise; // Await the resolution of the promise console.log(result); } catch (error) { console.error(error); } }; // Execute the async function handleMyPromise(); ``` ### 3.6.1 Handling Multiple Promises Concurrently ```typescript const fetchAllData = async (): Promise<void> => { try { const [data1, data2] = await Promise.all([ fetch('https://api.example1.com').then(res => res.json()), fetch('https://api.example2.com').then(res => res.json()), ]); console.log(data1, data2); } catch (error) { console.error("Error fetching data:", error); } }; //--------- Or const fetchJsonData = async (url: string) => { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch data from ${url}: ${response.statusText}`); } return await response.json(); }; const fetchAllDataConcurrently = async (): Promise<void> => { try { const [data1, data2] = await Promise.all([ fetchJsonData('https://api.example1.com'), fetchJsonData('https://api.example2.com'), ]); console.log(data1, data2); } catch (error) { console.error("Error fetching data:", error); } }; // Invoke the async function fetchAllDataConcurrently(); ``` ### 3.6.2 Sequential Promise Execution ```typescript const fetchAllData = async (): Promise<void> => { try { const [data1, data2] = await Promise.all([ fetch('https://api.example1.com').then(res => res.json()), fetch('https://api.example2.com').then(res => res.json()), ]); console.log(data1, data2); } catch (error) { console.error("Error fetching data:", error); } }; //--------- or const fetchJsonData = async (url: string) => { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch data from ${url}: ${response.statusText}`); } return await response.json(); }; const fetchSequentialData = async (): Promise<void> => { try { const data1 = await fetchJsonData('https://api.example1.com'); const data2 = await fetchJsonData('https://api.example2.com'); console.log(data1, data2); } catch (error) { console.error("Error fetching data sequentially:", error); } }; // Invoke the async function fetchSequentialData(); ``` ### Other example 1: Processing Files Asynchronously ```typescript // Assuming these are the accessible URLs to files served over HTTP const fileURLs = [ '/file1.txt', '/file2.txt' ]; const processFile = async (fileUrl: string): Promise<string> => { try { const response = await fetch(fileUrl); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.text(); return data } catch (error) { console.error(`Error processing file ${fileUrl}:`, error); throw error; // Rethrow or handle as needed } }; const processAllFiles = async (): Promise<string[]> => { const promises = fileURLs.map(fileUrl => processFile(fileUrl)); return Promise.all(promises); }; // Invoke the async function processAllFiles() .then(results => console.log(results)) .catch(error => console.error("Failed to process files:", error)); ``` ### Other example 2: Delayed Operations in Sequence Let's say you have an array of items, and for each item, you want to perform an operation that requires waiting for a certain amount of time (a simulated delay) before proceeding to the next item. This pattern is useful for rate-limiting or when dealing with APIs that have usage constraints. ```typescript const items = [1, 2, 3, 4, 5]; const delayedOperation = async (item: number): Promise<number> => { return new Promise(resolve => { setTimeout(() => { console.log(`Processed item ${item}`); resolve(item * 2); // Example operation }, 1000 * item); // Delay increases with each item }); }; const processItemsSequentially = async (): Promise<number[]> => { const results = []; for (const item of items) { const result = await delayedOperation(item); results.push(result); } return results; }; // Invoke the async function processItemsSequentially() .then(results => console.log("Final results:", results)) .catch(error => console.error("Failed to process items:", error)); ``` #### Example mimics the multi-step sign-up process using Delayed Operations in Sequence In this scenario, we have a series of validation steps that must be completed sequentially. After each step, the user receives feedback on the progress. We'll simulate these steps with asynchronous functions that resolve after a delay, representing time-consuming operations like server validations or database checks. ```typescript type ValidationStepResult = { step: number; message: string; isValid: boolean; }; const simulateValidationStep = async (step: number): Promise<ValidationStepResult> => { return new Promise((resolve) => { setTimeout(() => { const isValid = Math.random() > 0.2; // Simulate a validation check with an 80% chance to pass const message = isValid ? `Step ${step} completed successfully.` : `Step ${step} failed validation.`; console.log(message); // In a real app, you would update the UI instead resolve({ step, message, isValid }); }, 1000 * step); // Simulate a delay for each validation step }); }; const validateSignUpProcess = async (): Promise<void> => { const steps = [1, 2, 3, 4]; // Representing each step of the signup/validation process let allStepsValid = true; for (const step of steps) { const result = await simulateValidationStep(step); if (!result.isValid) { allStepsValid = false; break; // Stop the validation process if any step fails } } if (allStepsValid) { console.log("All steps completed successfully. User successfully signed up."); // Proceed with the next step in the signup process, e.g., creating the user account } else { console.log("Signup process halted due to failed validation step."); // Provide feedback to the user and potentially allow them to correct the input } }; // Invoke the validation process validateSignUpProcess().catch(error => console.error("Signup validation process encountered an error:", error)); //example console for all step success senarios // main.js:7 Step 1 completed successfully. // main.js:7 Step 2 completed successfully. // main.js:7 Step 3 completed successfully. // main.js:7 Step 4 completed successfully. // main.js:23 All steps completed successfully. User successfully signed up. //example console for a fail senario // main.js:7 Step 1 completed successfully. // main.js:7 Step 2 failed validation. // main.js:27 Signup process halted due to failed validation step. ``` ## 3.7 Payload validation with TypeScript ### 3.7.1 Validate user input #### 1. Type Guards Example 1: ```typescript interface LoginForm { username: string; password: string; } // Type guard for LoginForm function isLoginForm(data: any): data is LoginForm { return typeof data === 'object' && typeof data.username === 'string' && typeof data.password === 'string'; } // Sample usage const formData = { username: 'user123', password: 'pass123' }; // Assume this comes from user input if (isLoginForm(formData)) { console.log("Valid login form data:", formData); } else { console.error("Invalid login form data"); } ``` Example 2: File Upload Validation ```typescript interface UploadedFile { size: number; // File size in bytes type: string; // MIME type } // Type guard for UploadedFile function isValidFile(file: any): file is UploadedFile { const maxSize = 5 * 1024 * 1024; // 5 MB const allowedTypes = ['image/jpeg', 'image/png', 'text/plain']; return typeof file === 'object' && typeof file.size === 'number' && file.size <= maxSize && typeof file.type === 'string' && allowedTypes.includes(file.type); } // Sample usage with a mock file object const file = { size: 300000, type: 'image/jpeg' }; // Assume this comes from an <input type="file" /> if (isValidFile(file)) { console.log("File is valid for upload:", file); } else { console.error("File is invalid"); } ``` #### 2. Type Generation from JSON Create a JSON file (userInputRules.json) defining the expected structure of data from an API call: In `tsconfig.json` ```json ... "resolveJsonModule": true, ... ``` `userInputRules.json` ```json { "username": { "minLength": 3, "maxLength": 20 }, "password": { "minLength": 8, "requireSpecialCharacter": true } } ``` ```typescript import * as userInputRules from './userInputRules.json'; type UserInputRules = typeof userInputRules; function validateUserInput(input: { username: string; password: string }, rules: UserInputRules): boolean { // Simplified example of using the rules for validation const { username, password } = input; if (username.length < rules.username.minLength || username.length > rules.username.maxLength) { console.error("Username validation failed."); return false; } // Example check for a special character in the password const hasSpecialCharacter = /[!@#$%^&*(),.?":{}|<>]/g.test(password); if (password.length < rules.password.minLength || (rules.password.requireSpecialCharacter && !hasSpecialCharacter)) { console.error("Password validation failed."); return false; } return true; } // Sample usage const userInput = { username: "user1", password: "pass!word123" }; validateUserInput(userInput, userInputRules); ``` #### 3. library: yup ```typescript interface UserRegistrationForm { username: string; password: string; email: string; } //---- import * as yup from 'yup'; const registrationSchema = yup.object({ username: yup.string().required().min(3).max(20), password: yup.string() .min(8, 'Password must be at least 8 characters long') .matches(/[!@#$%^&*(),.?":{}|<>]/, 'Password must contain at least one special character') .required('Password is required'); email: yup.string().email().required(), }); const validateUserRegistration = async (formData: any): Promise<UserRegistrationForm> => { try { return await registrationSchema.validate(formData); } catch (error) { console.error("Validation error:", error); throw error; } }; ``` ### 3.7.2 Validating API Response Data #### 1. Typeguards ```typescript interface UserProfile { id: number; name: string; email: string; } // Type guard for UserProfile function isUserProfile(data: any): data is UserProfile { return typeof data === 'object' && typeof data.id === 'number' && typeof data.name === 'string' && typeof data.email === 'string'; } // Simulated API call async function fetchUserProfile(userId: number): Promise<UserProfile | null> { const response = await fetch(`https://api.example.com/users/${userId}`); const data = await response.json(); if (isUserProfile(data)) { return data; } else { console.error("Invalid user profile data"); return null; } } ``` #### 2. Type Generation from JSON Create a JSON file (apiDataSchema.json) defining the expected structure of data from an API call: In `tsconfig.json` ```json ... "resolveJsonModule": true, ... ``` `apiDataSchema.json` ```json { "userId": "number", "name": "string", "email": "string" } ``` ```typescript import {default as apiDataSchema} from './apiDataSchema.json'; type APIDataSchema = typeof apiDataSchema; async function fetchAndValidateUserData(url: string, schema: APIDataSchema): Promise<any> { const response = await fetch(url); // const userData = await response.json(); //check data: const userData = { userId: 1, name:"John Cena" email: "john.cena@gmail.com" } // Check if all fields in userData match the schema const isValid = Object.entries(schema).every(([field, type]) => typeof userData[field] === type); if (!isValid) { console.error("API data validation failed."); return null; } return userData; } // Sample usage const apiUrl = "https://api.example.com/user/1"; fetchAndValidateUserData(apiUrl, apiDataSchema).then(data => { if (data) { console.log("Validated user data:", data); } }); ``` #### 3. Using library: yup ```typescript import * as yup from 'yup'; interface ApiResponse { userId: number; username: string; email: string; } const registrationSchema = yup.object({ username: yup.string().required().min(3), password: yup.string().required().min(8), email: yup.string().email().required(), }); const fetchUserData = async (userId: number): Promise<ApiResponse> => { const response = await fetch(`https://api.example.com/users/${userId}`); const data = await response.json(); try { await registrationSchema.validate(data); } catch (error) { throw new Error("Invalid API response format."); } return data as ApiResponse; }; ```