Introduction

Welcome to the first chapter of your comprehensive TypeScript interview preparation guide! This chapter, “TypeScript Fundamentals & Core Type System,” lays the essential groundwork for understanding TypeScript at any level. It focuses on the foundational concepts that every TypeScript developer, from entry-level to architect, must master.

Here, we’ll delve into the core principles of TypeScript’s type system, including structural typing, type inference, fundamental types, and the crucial distinctions between various type constructs. Mastering these concepts is vital because they form the bedrock upon which all advanced TypeScript patterns and architectural decisions are built. Interviewers often start with these basics to gauge a candidate’s fundamental understanding before moving to more complex topics. This chapter is particularly relevant for entry to mid-level professionals but serves as a critical refresher and deep dive for senior and architect roles.

Core Interview Questions

Q1: What is TypeScript, and what are its primary benefits in modern web development?

A: TypeScript, as of January 2026, is a strongly typed superset of JavaScript that compiles to plain JavaScript. Developed and maintained by Microsoft, its primary goal is to bring static typing to JavaScript applications, enabling developers to catch errors early during development rather than at runtime.

Primary Benefits:

  1. Early Error Detection: The compiler identifies type-related errors before the code runs, significantly reducing bugs in production.
  2. Improved Code Quality & Maintainability: Explicit types make code easier to read, understand, and refactor, especially in large and complex codebases.
  3. Enhanced Developer Experience: Modern IDEs leverage TypeScript’s type information to provide powerful features like intelligent code completion, real-time error checking, and robust refactoring tools.
  4. Better Collaboration: Types act as documentation, clarifying the expected data structures and function signatures, which aids team collaboration.
  5. Scalability: TypeScript is particularly beneficial for large-scale applications, where maintaining consistency and preventing type-related issues becomes critical.
  6. ES Next Features: TypeScript often supports upcoming ECMAScript features (like decorators, optional chaining, nullish coalescing) before they are widely adopted in JavaScript runtimes, providing a transpilation layer.

Key Points:

  • Superset of JavaScript.
  • Compiles to plain JavaScript.
  • Static typing.
  • Focus on developer experience and error prevention.

Common Mistakes:

  • Describing it as a new language instead of a superset.
  • Overlooking the developer tooling benefits.
  • Not mentioning its compilation step.

Follow-up: How does TypeScript achieve static typing when JavaScript is dynamically typed?

Q2: Explain the concept of “Structural Typing” in TypeScript. How does it differ from “Nominal Typing”?

A: TypeScript employs structural typing (also known as “duck typing”). This means that two types are considered compatible if they have the same structure (i.e., the same properties and methods with compatible types), regardless of their name or origin. If an object has all the required properties of a certain type, TypeScript considers it to be of that type.

For example:

interface Point {
  x: number;
  y: number;
}

interface AnotherPoint {
  x: number;
  y: number;
}

let p1: Point = { x: 10, y: 20 };
let p2: AnotherPoint = p1; // This is allowed because p1 has the structure of AnotherPoint

In contrast, nominal typing (common in languages like Java, C#, or C++) considers types compatible only if they have the same name or are explicitly related through inheritance. Even if two types have identical structures, they are considered different if their names or declarations differ.

Key Points:

  • Structural typing: based on shape/structure.
  • Nominal typing: based on declaration name/identity.
  • TypeScript’s flexibility comes from structural typing.

Common Mistakes:

  • Confusing structural typing with dynamic typing.
  • Not providing a clear example to illustrate the difference.
  • Incorrectly stating that TypeScript uses nominal typing.

Follow-up: Can you provide a real-world scenario where structural typing might lead to unexpected behavior or where nominal typing might be preferred?

Q3: Describe TypeScript’s Type Inference. When does it occur, and why is it beneficial?

A: Type inference is TypeScript’s ability to automatically deduce the type of a variable, function return, or expression without explicit type annotations. The TypeScript compiler analyzes the code and assigns the most specific type it can determine.

When it occurs:

  • Variable Initialization: let x = 5; (x is inferred as number)
  • Function Return Values: function add(a: number, b: number) { return a + b; } (return type inferred as number)
  • Array Literals: let arr = [1, 2, 3]; (arr is inferred as number[])
  • Object Literals: let obj = { name: "Alice", age: 30 }; (obj is inferred as { name: string; age: number; })

Benefits:

  1. Reduced Verbosity: Developers don’t need to explicitly type everything, leading to cleaner and less repetitive code.
  2. Increased Productivity: Saves time and effort by letting the compiler do the heavy lifting for common cases.
  3. Maintainability: When the initial value or usage changes, the inferred type often updates automatically, reducing the need for manual type annotation adjustments.
  4. Balance between Safety and Ergonomics: Provides type safety without requiring excessive explicit typing.

Key Points:

  • Automatic type deduction by the compiler.
  • Occurs during variable initialization, function returns, etc.
  • Reduces boilerplate, improves readability, and maintains safety.

Common Mistakes:

  • Assuming inference means no type checking occurs.
  • Not understanding when inference happens.
  • Confusing inference with any type (inference tries to be as specific as possible).

Follow-up: In what situations might you explicitly add a type annotation even when inference could handle it, and why?

Q4: Differentiate between any, unknown, void, and never types in TypeScript (version 5.x). Provide use cases for each.

A:

  • any: The most permissive type. A variable typed as any can hold any JavaScript value, and you can perform any operation on it without type checking. It effectively opts out of TypeScript’s type system for that variable.

    • Use Case: When migrating a large JavaScript codebase to TypeScript, any can be used as a temporary placeholder for parts of the code that haven’t been fully typed yet. It’s generally discouraged in new, type-safe code.
    let data: any = JSON.parse("some_json_string");
    data.foo.bar(); // No type error, even if foo or bar don't exist
    
  • unknown: Introduced in TypeScript 3.0, unknown is a type-safe counterpart to any. A variable of type unknown can hold any value, but you cannot perform operations on it (like calling methods or accessing properties) until its type has been narrowed through type guards.

    • Use Case: When receiving data from an external source (e.g., API response, user input) whose type is not immediately known or guaranteed. It forces you to perform type checks before using the data.
    let value: unknown = "Hello";
    // value.toUpperCase(); // Error: 'value' is of type 'unknown'.
    if (typeof value === 'string') {
      console.log(value.toUpperCase()); // OK, type narrowed to string
    }
    
  • void: Represents the absence of any type. It’s typically used as the return type for functions that do not return any value.

    • Use Case: Functions that perform side effects but don’t explicitly return data.
    function logMessage(message: string): void {
      console.log(message);
    }
    
  • never: Represents the type of values that never occur. It’s used for functions that throw an error or enter an infinite loop, or for cases where all possibilities have been exhausted in a type union.

    • Use Cases:
      1. Functions that always throw an exception:
        function error(message: string): never {
          throw new Error(message);
        }
        
      2. Exhaustiveness checking in conditional types or switch statements:
        type Shape = 'circle' | 'square';
        function assertNever(x: never): never {
          throw new Error("Unexpected object: " + x);
        }
        function getArea(shape: Shape) {
          switch (shape) {
            case 'circle': return 10;
            case 'square': return 20;
            default: return assertNever(shape); // Ensures all cases are handled
          }
        }
        

Key Points:

  • any: Opts out of type checking.
  • unknown: Type-safe any, requires narrowing.
  • void: Function returns no value.
  • never: Function never returns, or for exhaustiveness checking.

Common Mistakes:

  • Using any when unknown would be safer.
  • Confusing void with undefined (though a function returning void can implicitly return undefined).
  • Not understanding never’s role in exhaustiveness checking.

Follow-up: When would you absolutely have to use any in a modern TypeScript project, and how would you mitigate its risks?

Q5: Explain the difference between Type Aliases and Interfaces in TypeScript 5.x. When would you choose one over the other?

A: Both Type Aliases and Interfaces are used to define the shape of objects or functions in TypeScript. However, they have distinct characteristics and preferred use cases.

Interface:

  • Declaration Merging: Interfaces can be declared multiple times with the same name, and TypeScript will automatically merge their definitions. This is particularly useful when extending libraries or defining types across different files.
  • Extend/Implement: Interfaces can extend other interfaces and classes, and classes can implement interfaces.
  • Object Shapes: Primarily designed for defining the shape of objects.
  • Performance: Historically, interfaces were slightly more performant in certain scenarios, but this difference is largely negligible in modern TypeScript versions (5.x).

Type Alias:

  • No Declaration Merging: Type aliases cannot be merged. If you declare two type aliases with the same name, it will result in a compilation error.
  • Can Refer to Primitives, Unions, Tuples, etc.: Type aliases are more versatile; they can define names for any type, including primitives (string, number), union types (string | number), intersection types (TypeA & TypeB), tuples ([string, number]), and complex mapped types.
  • typeof & keyof: Often used in conjunction with typeof and keyof to create dynamic types.

When to choose:

  • Prefer interface for:
    • Defining object shapes, especially for publicly exposed API contracts.
    • When you need declaration merging (e.g., augmenting a third-party library’s types).
    • When you want to use implements with a class.
  • Prefer type for:
    • Defining aliases for primitive types, union types, intersection types, or tuple types.
    • When you need to use advanced type features like conditional types, mapped types, or template literal types.
    • When you want to create a type that’s a combination of other types using & or |.

Key Points:

  • Interfaces merge, types do not.
  • Interfaces primarily for object shapes; types for any type.
  • Classes implement interfaces.
  • Types are more flexible for advanced type manipulations.

Common Mistakes:

  • Believing they are completely interchangeable.
  • Not knowing about declaration merging for interfaces.
  • Incorrectly stating one is always “better” than the other.

Follow-up: Can you demonstrate a scenario where declaration merging for interfaces is beneficial?

Q6: What are Union and Intersection Types in TypeScript? Provide examples of how they are used.

A:

  • Union Types (|): A union type allows a value to be one of several types. If a variable is of a union type, it can hold values of any of the types listed in the union. To operate on a union typed variable, you often need to use type narrowing.

    • Example:
      type StringOrNumber = string | number;
      let value: StringOrNumber = "hello";
      value = 123; // Both are valid
      
      function printId(id: string | number) {
        if (typeof id === "string") {
          console.log(id.toUpperCase()); // id is narrowed to string
        } else {
          console.log(id.toFixed(2)); // id is narrowed to number
        }
      }
      
  • Intersection Types (&): An intersection type combines multiple types into one. A value of an intersection type must possess all the properties and methods of all the types being intersected. It essentially creates a new type that has the members of all combined types.

    • Example:
      interface Person {
        name: string;
      }
      
      interface Employee {
        employeeId: number;
      }
      
      type BusinessPerson = Person & Employee;
      
      let user: BusinessPerson = {
        name: "Alice",
        employeeId: 1001
      };
      // user must have both 'name' and 'employeeId' properties
      
      If there are conflicting non-any types for the same property name, the resulting property type becomes never.

Key Points:

  • Union (|): “OR” relationship, value can be any of the types. Requires narrowing for specific operations.
  • Intersection (&): “AND” relationship, value must be all of the types.

Common Mistakes:

  • Confusing the behavior of union and intersection types (e.g., thinking a union type requires properties from all types).
  • Not understanding the need for type narrowing with union types.
  • Incorrectly assuming intersection types will merge conflicting properties gracefully (they often result in never).

Follow-up: How would you handle a scenario where two interfaces in an intersection type have the same property name but different, incompatible types (e.g., prop: string and prop: number)?

Q7: Explain Type Narrowing in TypeScript. Provide examples of common narrowing techniques.

A: Type Narrowing is the process by which TypeScript’s compiler refines a broader type into a more specific one within a certain code block. This typically happens through checks that eliminate possibilities for a variable’s type. Once narrowed, TypeScript allows you to safely access properties or call methods specific to the narrower type.

Common Narrowing Techniques:

  1. typeof Type Guards: Checks the JavaScript runtime type of a value.

    function processInput(input: string | number) {
      if (typeof input === 'string') {
        console.log(input.toUpperCase()); // input is narrowed to string
      } else {
        console.log(input.toFixed(2)); // input is narrowed to number
      }
    }
    
  2. instanceof Type Guards: Checks if a value is an instance of a particular class.

    class Dog { bark() {} }
    class Cat { meow() {} }
    function animalSound(animal: Dog | Cat) {
      if (animal instanceof Dog) {
        animal.bark(); // animal is narrowed to Dog
      } else {
        animal.meow(); // animal is narrowed to Cat
      }
    }
    
  3. in Operator Type Guards: Checks if an object has a certain property.

    interface Car { drive(): void; }
    interface Boat { sail(): void; }
    function operateVehicle(vehicle: Car | Boat) {
      if ('drive' in vehicle) {
        vehicle.drive(); // vehicle is narrowed to Car
      } else {
        vehicle.sail(); // vehicle is narrowed to Boat
      }
    }
    
  4. Equality Narrowing: Using ==, ===, !=, !== to compare values. This is particularly useful for checking null or undefined.

    function greet(name: string | null) {
      if (name !== null) {
        console.log(`Hello, ${name.toUpperCase()}`); // name is narrowed to string
      }
    }
    
  5. Truthiness Narrowing: Using if statements with non-null/non-undefined checks.

    function printLength(str: string | undefined) {
      if (str) { // str is narrowed to string if it's not undefined or empty string
        console.log(str.length);
      }
    }
    
  6. User-Defined Type Guards: Functions that return a type predicate (parameter is Type).

    interface Fish { swim(): void; }
    interface Bird { fly(): void; }
    function isFish(pet: Fish | Bird): pet is Fish {
      return (pet as Fish).swim !== undefined;
    }
    function move(pet: Fish | Bird) {
      if (isFish(pet)) {
        pet.swim(); // pet is narrowed to Fish
      } else {
        pet.fly(); // pet is narrowed to Bird
      }
    }
    

Key Points:

  • Refines broader types to specific types.
  • Enables safe operations on union types.
  • Various techniques: typeof, instanceof, in, equality, truthiness, user-defined guards.

Common Mistakes:

  • Forgetting to narrow types when working with union types, leading to compilation errors.
  • Misunderstanding that narrowing happens at compile-time, not runtime (though it’s based on runtime checks).
  • Over-reliance on type assertions (as Type) instead of proper narrowing.

Follow-up: When would you use a user-defined type guard over an in operator type guard?

Q8: What is tsconfig.json, and what are some essential compiler options for a modern TypeScript 5.x project?

A: tsconfig.json is a configuration file that specifies the root files and the compiler options required to compile a TypeScript project. It tells the TypeScript compiler (tsc) how to compile your code, including which files to include, which files to exclude, and how to behave during compilation.

Essential Compiler Options for a Modern TypeScript 5.x Project:

  1. "target": "es2022" (or newer): Specifies the ECMAScript target version for the output JavaScript. es2022 provides good compatibility with modern runtimes and browsers while supporting recent language features.
  2. "module": "Node16" (or "ESNext", "ES2022"): Specifies the module code generation strategy. Node16 is recommended for Node.js projects as it correctly handles package.json#exports and type fields. For browser-based or bundler-driven projects, ESNext or ES2022 are common.
  3. "strict": true: Enables a broad range of type-checking strictness options. This is highly recommended for all new projects and is crucial for robust type safety. It includes:
    • noImplicitAny: Flags expressions and declarations with an implicitly any type.
    • strictNullChecks: Ensures that null and undefined cannot be assigned to types that don’t explicitly allow them.
    • strictFunctionTypes: Ensures function parameters are contravariantly checked.
    • strictPropertyInitialization: Ensures class properties are initialized in the constructor.
    • noImplicitThis: Flags this expressions with an implicitly any type.
    • alwaysStrict: Parse in strict mode and emit “use strict” for each source file.
  4. "esModuleInterop": true: Enables compatibility between CommonJS and ES Modules. It allows you to use import React from "react" even if React was originally exported with module.exports.
  5. "forceConsistentCasingInFileNames": true: Disallows inconsistent casing in file paths, preventing issues on case-sensitive file systems.
  6. "skipLibCheck": true: Skips type checking of declaration files (.d.ts). This can speed up compilation and avoid issues with third-party libraries that might have minor type errors, but it means you trust the library’s types.
  7. "outDir": "./dist": Specifies the output directory for compiled JavaScript files.
  8. "rootDir": "./src": Specifies the root directory of TypeScript files.
  9. "declaration": true: (For libraries) Generates .d.ts declaration files for your modules, allowing other TypeScript projects to consume your library with type safety.
  10. "jsx": "react-jsx" (or "react"): If using React, specifies the JSX emit mode. react-jsx is the modern approach (since React 17) that doesn’t require import React from 'react'.

Key Points:

  • Configures TypeScript compiler behavior.
  • strict: true is paramount for type safety.
  • Modern target and module options.
  • esModuleInterop for module compatibility.

Common Mistakes:

  • Not enabling "strict": true from the start of a project.
  • Misunderstanding the interaction between target and module options.
  • Ignoring forceConsistentCasingInFileNames, leading to build issues on different OS.

Follow-up: If you’re building a library that will be consumed by other TypeScript projects, which tsconfig.json option is critical to enable, and why?

Q9: What are TypeScript Declaration Files (.d.ts) and why are they important?

A: TypeScript Declaration Files, identified by the .d.ts extension, are files that describe the shape of existing JavaScript code. They contain only type information (interfaces, type aliases, function signatures, variable declarations, etc.) and no executable code. The TypeScript compiler uses these files to understand the types in JavaScript modules, allowing it to provide type checking, IntelliSense, and other language services for JavaScript libraries and frameworks.

Why they are important:

  1. Type Safety for JavaScript Libraries: They allow TypeScript projects to safely consume plain JavaScript libraries by providing type definitions for their APIs. Without .d.ts files, all imports from JavaScript libraries would implicitly be any, losing all type benefits.
  2. Interoperability: They bridge the gap between the statically typed TypeScript world and the dynamically typed JavaScript world.
  3. Tooling Support: IDEs (like VS Code) read .d.ts files to offer autocompletion, signature help, and navigation for JavaScript modules, significantly enhancing the developer experience.
  4. Documentation: They serve as excellent documentation for the API of a library, clearly outlining what parameters functions expect, what types they return, and the structure of objects.
  5. Authoring Libraries: When authoring a TypeScript library that will be published, generating .d.ts files (using the declaration: true compiler option) is crucial so that consumers of your library get type safety.

Key Points:

  • Contain only type definitions, no executable code.
  • Bridge JavaScript and TypeScript type systems.
  • Enable type checking and tooling for JS libraries.
  • Crucial for publishing TypeScript libraries.

Common Mistakes:

  • Thinking .d.ts files contain actual JavaScript code.
  • Not understanding their role in providing type safety for external JavaScript.
  • Neglecting to generate them when publishing a TypeScript library.

Follow-up: How does TypeScript find and use .d.ts files for a given JavaScript library, especially for third-party packages installed via npm?

Q10: When would you use a Type Assertion (as Type) versus a Type Cast in other languages? What are the risks?

A: In TypeScript, the term “Type Assertion” is used, rather than “type casting” (which implies runtime conversion). A type assertion tells the TypeScript compiler, “Trust me, I know this value is of this type.” It’s a way to override TypeScript’s inferred or assigned type for a variable when you have more specific knowledge than the compiler. It has no runtime effect and is purely a compile-time construct.

When to use it:

  • Narrowing a Union Type: When you know a variable’s type more specifically than TypeScript can infer, especially after some runtime check not understood by TS.
    const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
    // TypeScript doesn't know this is specifically a canvas element,
    // but you do, so you assert it.
    
  • Working with any or unknown: After performing a runtime check on an unknown type, you might assert its specific type to avoid further narrowing checks.
    let value: unknown = "hello";
    const len = (value as string).length; // Assert it's a string to access .length
    
  • React Event Handling: Asserting the type of event.target in event handlers.

Risks:

  1. Runtime Errors: If your assertion is incorrect (the value is not actually of the asserted type at runtime), you will encounter runtime errors (e.g., trying to call a method that doesn’t exist on the actual object). TypeScript will not catch this because you’ve told it to trust you.
  2. Masking Bugs: Incorrect assertions can hide actual type mismatches, preventing TypeScript from doing its job of finding potential bugs.
  3. Reduced Type Safety: Overuse of type assertions can undermine the benefits of TypeScript’s static type checking.

Comparison to Type Casting in other languages: In languages like Java or C#, type casting often involves a runtime check and potentially a runtime conversion (e.g., downcasting an object to a subclass). If the cast fails, it results in a runtime error (e.g., ClassCastException). TypeScript’s type assertion, however, is solely a compile-time hint and generates no runtime code or checks.

Key Points:

  • Compile-time construct, no runtime effect.
  • “Trust me, compiler.”
  • Use when you have more specific type knowledge.
  • Risks: Runtime errors if incorrect, masks bugs, reduces type safety.

Common Mistakes:

  • Using type assertions as a shortcut to avoid proper type narrowing.
  • Believing type assertions perform runtime type checks.
  • Overusing them, especially with any, which defeats the purpose of TypeScript.

Follow-up: What is “double assertion” (as any as Type), and why might someone use it (and why should it generally be avoided)?

MCQ Section

Q1: Which of the following best describes TypeScript’s type system?

A. Nominally typed B. Dynamically typed C. Structurally typed D. Weakly typed

Correct Answer: C Explanation:

  • A. Nominally typed: Incorrect. Nominal typing relies on explicit declarations and names for type compatibility, which TypeScript does not primarily use.
  • B. Dynamically typed: Incorrect. JavaScript is dynamically typed; TypeScript adds static typing on top of it.
  • C. Structurally typed: Correct. TypeScript determines type compatibility based on the shape (structure) of objects, not their declared name.
  • D. Weakly typed: Incorrect. Weak typing implies implicit type conversions, which JavaScript exhibits, but TypeScript aims to provide stronger type safety.

Q2: What is the primary benefit of using unknown over any in TypeScript?

A. unknown allows more operations without type checking. B. unknown ensures runtime type safety, whereas any does not. C. unknown forces type narrowing before operations can be performed. D. unknown is a primitive type, while any is a complex type.

Correct Answer: C Explanation:

  • A. unknown allows more operations without type checking: Incorrect. The opposite is true; unknown requires narrowing.
  • B. unknown ensures runtime type safety, whereas any does not: Incorrect. Neither any nor unknown provides runtime type safety directly; they are compile-time constructs. unknown encourages you to add runtime checks.
  • C. unknown forces type narrowing before operations can be performed: Correct. This is the key advantage; it makes your code safer by requiring explicit checks.
  • D. unknown is a primitive type, while any is a complex type: Incorrect. Both are special types in TypeScript’s type system, not primitive or complex in the traditional sense.

A. "noImplicitAny": true B. "esModuleInterop": true C. "strict": true D. "target": "es2022"

Correct Answer: C Explanation:

  • A. "noImplicitAny": true: This is part of what "strict": true enables, but "strict" includes many more critical checks.
  • B. "esModuleInterop": true: This is for module compatibility, not general strictness.
  • C. "strict": true: Correct. This single option activates several strict type-checking flags, including noImplicitAny, strictNullChecks, strictFunctionTypes, and more, making your codebase much safer.
  • D. "target": "es2022": This specifies the output JavaScript version, not type-checking strictness.

Q4: Consider the following TypeScript code:

function processValue(value: string | number) {
  if (typeof value === 'string') {
    // A
  } else {
    // B
  }
}

In section A, what is the type of value? A. string | number B. string C. number D. any

Correct Answer: B Explanation:

  • A. string | number: Incorrect. This is the initial type before narrowing.
  • B. string: Correct. Inside the if (typeof value === 'string') block, TypeScript’s type narrowing mechanism correctly identifies value as a string.
  • C. number: Incorrect. This would be the type in section B (the else block).
  • D. any: Incorrect. TypeScript is actively performing type checking here.

Q5: What is the primary purpose of a .d.ts file in TypeScript?

A. To contain executable JavaScript code that TypeScript compiles. B. To provide runtime type checks for JavaScript variables. C. To declare types for existing JavaScript code, enabling type checking and tooling. D. To define new TypeScript features that are not yet standard.

Correct Answer: C Explanation:

  • A. To contain executable JavaScript code that TypeScript compiles: Incorrect. .d.ts files contain only type declarations, no executable code.
  • B. To provide runtime type checks for JavaScript variables: Incorrect. TypeScript’s type system is compile-time only. Runtime checks must be explicitly coded in JavaScript.
  • C. To declare types for existing JavaScript code, enabling type checking and tooling: Correct. This is their core function, allowing TypeScript to understand and interact safely with JavaScript libraries.
  • D. To define new TypeScript features that are not yet standard: Incorrect. New features are integrated directly into the TypeScript language and compiler, not via .d.ts files.

Mock Interview Scenario: Onboarding a New Feature with Core Types

Scenario Setup: You’re interviewing for a mid-level Frontend Developer position. The interviewer presents a common scenario: your team is adding a new “User Notification” feature to an existing application. You need to define the data structures for notifications and a function to display them. The application primarily uses React with TypeScript (version 5.x).

Interviewer: “Welcome! Let’s imagine you’re starting on a new task. We need to implement a user notification system. Notifications can either be simple messages (string) or a more complex object containing a title, message, and an optional URL for more details. We also need a function to ‘display’ these notifications. How would you approach defining the types for these notifications and the display function using TypeScript’s core features?”

Expected Flow of Conversation:

  1. Defining Notification Types (Union & Interface/Type Alias):

    • Candidate: “Okay, for the notification data, since it can be either a simple string or a complex object, a Union Type (|) immediately comes to mind. I’d define the complex object using an Interface or Type Alias.”
    • Candidate (Code Idea):
      interface ComplexNotification {
        title: string;
        message: string;
        url?: string; // Optional URL
      }
      
      type NotificationContent = string | ComplexNotification;
      
    • Interviewer (Follow-up): “Why did you choose interface for ComplexNotification and type for NotificationContent? Could you have used type for both, or interface for both?”
    • Candidate: “I chose interface for ComplexNotification because it’s a clear object shape, and interfaces are generally preferred for defining object contracts due to their ability for declaration merging, though it’s not strictly necessary here. For NotificationContent, type is more suitable because it’s a union of two distinct types (string and ComplexNotification), and interface cannot define unions. Yes, I could have used type ComplexNotification = { title: string; message: string; url?: string; }; and it would work perfectly fine, but interface is often idiomatic for object definitions.”
  2. Implementing the Display Function (Type Narrowing):

    • Interviewer: “Great. Now, how would you implement the displayNotification function that accepts NotificationContent and correctly handles both the string and object forms?”
    • Candidate: “To handle the different types within the function, I’d use Type Narrowing. Specifically, I’d check the typeof the content parameter.”
    • Candidate (Code Idea):
      function displayNotification(content: NotificationContent): void {
        if (typeof content === 'string') {
          console.log(`Simple Notification: ${content}`);
          // In a real app, this would show a toast or alert
        } else {
          // 'content' is now narrowed to ComplexNotification
          console.log(`Title: ${content.title}`);
          console.log(`Message: ${content.message}`);
          if (content.url) {
            console.log(`More Info: ${content.url}`);
          }
        }
      }
      
    • Interviewer (Follow-up): “What if the ComplexNotification could also have an id property that’s optional? How would that change your displayNotification function, if at all, assuming you only cared about title, message, and url for display?”
    • Candidate: “Adding an optional id?: string; to ComplexNotification wouldn’t change the displayNotification function’s logic for displaying the core message, title, and URL, because those properties remain the same. TypeScript’s structural typing would still ensure compatibility. If I did need to display the id, I would just access content.id within the else block, as content would still be correctly narrowed to ComplexNotification.”
  3. Handling Potential any/unknown (Architect/Senior Level add-on):

    • Interviewer: “Suppose this notification content might sometimes come from an external API, and initially, it’s typed as any or unknown. How would you approach that, and what are the trade-offs?”
    • Candidate: “If it’s unknown, that’s better than any. I’d immediately try to parse and validate it against NotificationContent. I’d likely use Type Guards to check if it’s a string or if it’s an object that has title and message properties. If it’s any, I’d still do the same validation, but any would allow me to bypass checks, which is risky. The trade-off is increased code verbosity for validation, but significantly higher runtime safety.”

Red Flags to Avoid:

  • Using any liberally: Suggesting any for the notification content without justification.
  • No type narrowing: Trying to access content.title directly on NotificationContent without checking its specific type.
  • Confusing interface and type without clear reasoning.
  • Not understanding optional properties (?).
  • Focusing only on runtime behavior and ignoring compile-time benefits.

Practical Tips

  1. Read the Official Docs: The TypeScript Handbook is an unparalleled resource. Pay close attention to the “Everyday Types” and “Narrowing” sections for this chapter’s topics.
  2. Practice on TypeScript Playground: Use the TypeScript Playground to experiment with types, see inference in action, and understand compiler errors. It’s an interactive way to solidify your understanding.
  3. Set up a Strict tsconfig.json: Start new projects with "strict": true enabled. This forces you to confront type issues early and learn best practices for handling null, undefined, and implicit any.
  4. Refactor JavaScript to TypeScript: Take small JavaScript modules or functions and convert them to TypeScript. This hands-on experience will highlight the benefits and challenges of adding types.
  5. Understand “Why”: Don’t just memorize definitions. Understand why TypeScript has structural typing, why unknown is safer than any, and why tsconfig.json options exist. This deeper understanding will help you answer scenario-based questions.
  6. Focus on Examples: For every concept, think of a simple, clear code example. Being able to illustrate your points with code is a strong indicator of understanding.

Summary

This chapter has covered the foundational elements of TypeScript’s type system, which are indispensable for any developer working with the language. We explored its core benefits, the distinction between structural and nominal typing, the power of type inference, and the critical roles of any, unknown, void, and never. We also delved into the practical applications of union and intersection types, various type narrowing techniques, and the essential configurations within tsconfig.json.

Mastering these fundamentals ensures you can write robust, maintainable, and type-safe code. It also prepares you to articulate complex type concepts clearly during interviews, demonstrating a solid grasp of TypeScript’s core philosophy. As you move forward, remember that a strong foundation here will greatly aid your understanding of more advanced TypeScript features and architectural patterns.


References Block:

  1. TypeScript Official Handbook: https://www.typescriptlang.org/docs/handbook/intro.html
  2. TypeScript tsconfig.json Reference: https://www.typescriptlang.org/tsconfig
  3. Understanding unknown in TypeScript: (Search for recent articles on Medium/Dev.to, e.g., “TypeScript unknown vs any 2024” or similar for current best practices). A good example is “The unknown Type in TypeScript” on DigitalOcean or similar reputable dev blogs.
  4. TypeScript Deep Dive (Book/Online Resource): https://basarat.gitbook.io/typescript/ (A highly respected community resource)
  5. Type Aliases vs. Interfaces in TypeScript: (Search for articles on specific differences, e.g., “TypeScript interface vs type alias 2024”). Many reputable blogs like LogRocket, freeCodeCamp, etc., cover this.

This interview preparation guide is AI-assisted and reviewed. It references official documentation and recognized interview preparation resources.