Try   HackMD

Working with TypeScript

IntersectionObserver


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

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

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

<div>
    <ul id="main">
        <li>hello</li>
        <li>world</li>
    </ul>    
</div>
// 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

// 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

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

// 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

// 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

// 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).
//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.

//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

//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.

//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.
const button = document.getElementById("myButton") as HTMLButtonElement;
button.addEventListener("click", (event) => {
  console.log("Button clicked!");
});

Type-Safe Event Handler:

button.addEventListener("click", (event: MouseEvent) => {
  console.log(event.clientX, event.clientY); // Access MouseEvent properties with type safety
});
  1. Using this in Event Handlers
    Logging the id of a button when it's clicked.
button.addEventListener("click", function(this: HTMLButtonElement) {
  this.classList.toggle('active');
  console.log(`Button ${this.id} state: ${this.classList.contains('active') ? 'Active' : 'Inactive'}`);
});
  1. Dealing with Different Event Types
    2.1 Handling Keyboard Events
    Example: Adding a keypress event to an input field.
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.

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.

//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

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

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

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

//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

// 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

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

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

// 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.

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.

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:

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

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

    ...
    "resolveJsonModule": true, 
    ...

userInputRules.json

{
  "username": {
    "minLength": 3,
    "maxLength": 20
  },
  "password": {
    "minLength": 8,
    "requireSpecialCharacter": true
  }
}
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

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

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

    ...
    "resolveJsonModule": true, 
    ...

apiDataSchema.json

{
  "userId": "number",
  "name": "string",
  "email": "string"
}
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

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;
};