Practice Questions

1. For each scenario below, would you use type or interface? Explain your reasoning.

  • (a) Defining the shape of a User object that other types may extend later
  • (b) Defining a value that can be either "loading", "success", or "error"
  • © Combining two existing object shapes into one (e.g., a UserWithPosts that has all fields from User and Posts)
Solution

(a) interface — When you need to define an object shape that other types will extend, use interface, because it supports the extends keyword. Interfaces can also be reopened and merged across files (declaration merging), so other code can add to the shape later.

interface User {
  name: string;
  age: number;
}

interface Admin extends User {
  permissions: string[];
}

(b) type — A string union is not an object shape, so interface cannot express it. type handles unions directly:

type Status = "loading" | "success" | "error";

© Either works, but type with intersection is more concise:

type UserWithPosts = User & { posts: Post[] };

You could also use interface extends, but intersections are more flexible when combining types that are not all interfaces.

The general guideline: use interface for object shapes (especially with inheritance) and type for unions, intersections, or aliasing primitives.

2. Given the following JavaScript variable declarations, add appropriate TypeScript type annotations to each:

const apiUrl = "https://api.example.com/data";
let isLoading = false;
let results = [];
const config = { timeout: 5000, retries: 3 };
Solution
const apiUrl: string = "https://api.example.com/data";
let isLoading: boolean = false;
let results: string[] = []; // or a more specific type like Result[]
const config: { timeout: number; retries: number } = {
  timeout: 5000,
  retries: 3,
};

Note that for const apiUrl, TypeScript can infer the type as the string literal "https://api.example.com/data" or as string. Explicit annotations are optional when TypeScript can infer the type from the assigned value, but they can improve readability and catch errors early.

3. What are optional properties in TypeScript? Write an interface called Profile that has a required username property (string) and optional bio and avatarUrl properties (both strings).

Solution

Optional properties are marked with ? after the property name. They indicate that the property may or may not be present on an object of that type.

interface Profile {
  username: string;
  bio?: string;
  avatarUrl?: string;
}

// Valid usages:
const user1: Profile = { username: "alice" };
const user2: Profile = { username: "bob", bio: "Developer" };
const user3: Profile = {
  username: "carol",
  bio: "Designer",
  avatarUrl: "https://example.com/avatar.png",
};

4. Write an async function called fetchData that takes a url (string), calls fetch, checks whether the response is OK, throws an error if not, and returns the parsed JSON. Then write a caller function that uses try/catch to handle errors.

Solution
const fetchData = async (url: string): Promise<unknown> => {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`Request failed (status ${response.status})`);
  }
  return response.json();
};

const loadItems = async (): Promise<void> => {
  try {
    const data = await fetchData("https://api.example.com/items");
    console.log(data);
  } catch (error) {
    console.error("Could not load items. Please try again.");
  }
};

The response.ok check is important because fetch only rejects on network errors, not on HTTP error statuses like 404 or 500. Without the check, you would pass an error response body along as if it were valid data.

5. What is a type assertion in TypeScript? Why would you use as HTMLInputElement instead of as HTMLElement when selecting an input element from the DOM?

Solution

A type assertion tells the TypeScript compiler to treat a value as a specific type when the compiler cannot infer it automatically. It does not change the runtime behavior; it only affects static type checking.

When you use document.getElementById("input"), TypeScript returns HTMLElement | null. If you assert it as HTMLElement, you get access to generic element properties but not input-specific ones like .value. Asserting as HTMLInputElement gives you access to .value, .placeholder, and other input-specific properties.

// This would cause a type error — HTMLElement has no .value property
const input = document.getElementById("input") as HTMLElement;
console.log(input.value); // Error!

// This works — HTMLInputElement has .value
const input2 = document.getElementById("input") as HTMLInputElement;
console.log(input2.value); // OK

TypeScript provides specific DOM types like HTMLInputElement, HTMLButtonElement, HTMLAudioElement, and HTMLSourceElement that extend HTMLElement with properties unique to each element type.

6. Explain what optional chaining (?.) and the nullish coalescing operator (??) do. Rewrite the following code using both operators:

let result;
if (data && data.items && data.items[0]) {
  result = data.items[0].name;
} else {
  result = "Unknown";
}
Solution

Optional chaining (?.) short-circuits and returns undefined if any part of the chain is null or undefined, instead of throwing a runtime error.

Nullish coalescing (??) returns the right-hand operand when the left-hand operand is null or undefined (but not for other falsy values like 0 or "").

Rewritten:

const result = data?.items?.[0]?.name ?? "Unknown";

This is equivalent to the original if/else block but much more concise. If data, items, the first element, or name is null or undefined at any point, the chain short-circuits and ?? provides the fallback value "Unknown".

7. What does “type erasure” mean in TypeScript? What are its implications for runtime behavior?

Solution

Type erasure means that all TypeScript type annotations, interfaces, and type aliases are removed during compilation to JavaScript. The compiled output contains no type information.

Implications:

  • No runtime type checking: TypeScript types are enforced only at compile time. Once the code runs in the browser or Node.js, there are no type guards provided by the type system itself.
  • No performance overhead: Since types are erased, they cost nothing at runtime.
  • You cannot use TypeScript types in runtime logic: For example, you cannot write if (typeof x === "MyInterface") because MyInterface does not exist at runtime. If you need runtime type checking, you must write explicit checks (e.g., checking for the presence of specific properties).

8. Write a generic function called getProperty that takes an object and a key, and returns the value at that key. Use TypeScript generics and keyof to ensure the key is valid for the given object type.

Solution
const getProperty = <T, K extends keyof T>(obj: T, key: K): T[K] => {
  return obj[key];
};

// Usage:
interface Book {
  title: string;
  pages: number;
  isbn?: string;
}

const myBook: Book = { title: "TypeScript Handbook", pages: 350 };

const title = getProperty(myBook, "title"); // string
const pages = getProperty(myBook, "pages"); // number
// getProperty(myBook, "author"); // Error: "author" is not assignable to "title" | "pages" | "isbn"
  • <T, K extends keyof T> declares two generic type parameters: T for the object type and K constrained to valid keys of T.
  • T[K] is a lookup type that resolves to the type of the property at key K, so the return type is automatically correct.
  • TypeScript will produce a compile-time error if you pass a key that does not exist on the object.

9. What is the void return type in TypeScript? How does it differ from undefined? Write a function with an explicit void return type.

Solution

The void type indicates that a function does not return a meaningful value. In JavaScript, such functions implicitly return undefined. TypeScript’s void makes this intent explicit: if you try to return a value from a void function, the compiler will report an error.

The difference from undefined: a function typed as returning undefined must explicitly return undefined;, while a void function can simply have no return statement.

const logMessage = (message: string): void => {
  console.log(message);
  // No return statement — this is correct for void
};

// This would cause a compile-time error:
// const logMessage = (message: string): void => {
//   console.log(message);
//   return message; // Error: Type 'string' is not assignable to type 'void'
// };

10. You are building a function that creates an HTML element, sets its class and text content, and returns it. Write this function in TypeScript with full type annotations:

// Create an element of type `tagName`, with the given CSS class and text.
// Example: createStyledElement("p", "text-bold", "Hello") returns a <p> element.
Solution
const createStyledElement = (
  tagName: string,
  className: string,
  text: string,
): HTMLElement => {
  const element = document.createElement(tagName);
  element.className = className;
  element.textContent = text;
  return element;
};

// Usage:
const heading = createStyledElement(
  "h1",
  "text-2xl font-semibold",
  "Definitions",
);
const paragraph = createStyledElement(
  "p",
  "text-gray-600",
  "No results found.",
);

Note that when tagName is a string variable (as in this helper), document.createElement returns HTMLElement. However, if you pass a literal tag name like "p" or "input" directly, TypeScript returns the specific element type (e.g., HTMLParagraphElement, HTMLInputElement) thanks to its overloaded type definitions. In this helper, since tagName is a generic string, TypeScript cannot narrow the return type further than HTMLElement.

11. Compare the two approaches to building dynamic HTML in the browser: (a) creating elements with document.createElement and assembling them programmatically, and (b) building an HTML string with template literals and assigning it via innerHTML. What are the trade-offs of each approach?

Solution

document.createElement approach:

  • Pros: Type-safe (TypeScript can check element types and properties), better for attaching event listeners directly to elements, avoids potential XSS vulnerabilities since text content is not parsed as HTML.
  • Cons: Verbose; it requires many helper functions to create, configure, and append elements one by one. Harder to visualize the resulting HTML structure from the code.

Template literals with innerHTML approach:

  • Pros: Concise and readable; the HTML structure is visible directly in the code. Fewer helper functions needed.
  • Cons: Potential XSS risk if user input is interpolated without sanitization. Replaces the entire DOM subtree, which can be less efficient and destroys any existing event listeners on child elements.

In general, use document.createElement for complex, interactive UIs, where it is safer and easier to maintain. Template literals with innerHTML are practical for simpler, read-only content.

12. What are source maps, and why are they necessary when debugging TypeScript? What configuration enables them?

Solution

Source maps are files that map the compiled JavaScript code back to the original TypeScript source. Browsers execute JavaScript, not TypeScript. Without source maps, a breakpoint set in a .ts file would not correspond to the correct line in the running code.

With source maps enabled, the browser’s debugger (or a VSCode debugger attached to Chrome) can show you the original TypeScript source, let you set breakpoints in .ts files, and display correct variable names and line numbers.

To enable source maps, add "sourceMap": true to the compilerOptions in tsconfig.json:

{
  "compilerOptions": {
    "sourceMap": true
  }
}

This tells the TypeScript compiler to generate .js.map files alongside the compiled JavaScript, which the browser uses to reconstruct the mapping.