Typescript Interview Questions and Answers

Find 100+ TypeScript interview questions and answers to assess candidates' skills in static typing, interfaces, classes, generics, and integrating TypeScript with JavaScript frameworks.
By
WeCP Team

As large-scale JavaScript applications require strong typing, better maintainability, and tooling support, TypeScript has become the industry standard for building robust, scalable, and error-free front-end and back-end applications. Recruiters must identify professionals skilled in TypeScript syntax, type system, and integration with frameworks like React, Angular, or Node.js.

This resource, "100+ TypeScript Interview Questions and Answers," is tailored for recruiters to simplify the evaluation process. It covers topics from TypeScript fundamentals to advanced type manipulations and real-world application design, including generics, decorators, and type inference.

Whether hiring for Front-End Developers, Full-Stack Engineers, or Node.js Developers, this guide enables you to assess a candidate’s:

  • Core TypeScript Knowledge: Understanding of basic types, interfaces, type aliases, enums, union and intersection types, and modules.
  • Advanced Skills: Expertise in generics, mapped types, conditional types, advanced type narrowing, decorators, and TypeScript configuration (tsconfig.json).
  • Real-World Proficiency: Ability to integrate TypeScript with React or Angular projects, refactor JavaScript codebases to TypeScript, write reusable and type-safe components, and configure TypeScript for build and deployment pipelines.

For a streamlined assessment process, consider platforms like WeCP, which allow you to:

Create customized TypeScript assessments aligned to your framework and project stack.
Include hands-on coding tasks, such as writing strongly typed functions, implementing generics, or building React components with TypeScript.
Proctor assessments remotely with AI-powered integrity checks.
Leverage automated grading to evaluate code correctness, type safety, and adherence to TypeScript best practices.

Save time, ensure code quality, and confidently hire TypeScript professionals who can build maintainable, scalable applications with fewer runtime errors from day one.

Typescript Interview Questions

Beginner Level Question

  1. What is TypeScript, and how does it differ from JavaScript?
  2. What are the benefits of using TypeScript over JavaScript?
  3. Explain the concept of "type annotations" in TypeScript.
  4. How do you declare a variable in TypeScript with a specific type?
  5. What is the any type in TypeScript, and when should it be used?
  6. How do you define a function in TypeScript with specific argument and return types?
  7. What is type inference in TypeScript?
  8. Explain the difference between let, const, and var in TypeScript.
  9. What are TypeScript's primitive types?
  10. What is the difference between undefined and null in TypeScript?
  11. How do you define an array in TypeScript?
  12. What is a tuple in TypeScript?
  13. What is an enum in TypeScript, and how is it different from a regular object?
  14. How do you define an object type in TypeScript?
  15. What are the differences between TypeScript interfaces and types?
  16. What is the void type in TypeScript?
  17. What does never mean in TypeScript, and when is it used?
  18. How does TypeScript handle type compatibility?
  19. What is a type alias, and how does it work in TypeScript?
  20. How do you define an optional property in TypeScript?
  21. What is the purpose of the readonly modifier in TypeScript?
  22. How do you define a function with optional parameters in TypeScript?
  23. How do you define default values for function parameters in TypeScript?
  24. What is the purpose of the as keyword in TypeScript?
  25. How do you use the typeof operator in TypeScript?
  26. What is a namespace in TypeScript, and how is it used?
  27. How do you perform type checking in TypeScript?
  28. What is the difference between interface and class in TypeScript?
  29. What are the different ways to import and export modules in TypeScript?
  30. Explain the concept of "declaration merging" in TypeScript.
  31. How do you use the strict mode in TypeScript?
  32. What is the unknown type in TypeScript?
  33. What are type guards in TypeScript?
  34. What are union types, and how are they used in TypeScript?
  35. How do you use the never type in a function that throws an error?
  36. What is a "literal type" in TypeScript, and how does it work?
  37. How do you use the keyof keyword in TypeScript?
  38. How do you handle third-party JavaScript libraries in TypeScript?
  39. What is the difference between declare and import in TypeScript?
  40. How do you enable TypeScript in a JavaScript project?

Intermediate Level Question

  1. What is the Partial<T> utility type in TypeScript, and how is it used?
  2. What is the Readonly<T> utility type in TypeScript, and how is it different from readonly properties?
  3. What is the Pick<T, K> utility type in TypeScript?
  4. How do you use generics in TypeScript to create reusable components?
  5. What is the extends keyword used for in TypeScript?
  6. How do you constrain a generic type in TypeScript?
  7. What are "mapped types" in TypeScript, and how are they useful?
  8. How does TypeScript handle asynchronous code, and what are Promise<T> and async/await in TypeScript?
  9. How do you use the Record<K, T> utility type in TypeScript?
  10. What is the purpose of the this keyword in TypeScript, and how does it differ from JavaScript?
  11. What is the difference between interface and abstract class in TypeScript?
  12. How do you implement inheritance in TypeScript?
  13. What is the difference between an "interface" and a "type alias" in TypeScript?
  14. What is the significance of the super keyword in TypeScript classes?
  15. How do you use constructor in TypeScript classes?
  16. What are "mixins" in TypeScript, and how do they work?
  17. How do you declare a read-only property in a class in TypeScript?
  18. How do you create and use custom decorators in TypeScript?
  19. What is the infer keyword in TypeScript, and how does it work?
  20. What is a conditional type in TypeScript?
  21. How does TypeScript support JSX/TSX?
  22. How do you perform type assertions in TypeScript?
  23. What are never and any types, and when should you use them in TypeScript?
  24. What is the difference between function and => function syntax in TypeScript?
  25. How does TypeScript handle module resolution?
  26. How do you use async/await with generics in TypeScript?
  27. What is the difference between type and interface when used with generics in TypeScript?
  28. What is the purpose of the declare keyword in TypeScript?
  29. How do you use the asserts keyword in TypeScript?
  30. What is a "destructuring" assignment in TypeScript, and how does it work?
  31. How do you define and use interfaces with optional properties in TypeScript?
  32. How do you handle errors and exceptions in TypeScript?
  33. What are module and namespace in TypeScript, and how do they differ?
  34. How do you perform runtime validation of types in TypeScript?
  35. How do you use the Exclude<T, U> utility type in TypeScript?
  36. What is the NonNullable<T> utility type, and when should it be used?
  37. How do you avoid common pitfalls in TypeScript type compatibility?
  38. What is the purpose of the void type in TypeScript, and when is it used?
  39. How do you create a type-safe event system in TypeScript?
  40. What is the difference between typeof and keyof in TypeScript?

Experienced Level Question

  1. How do you implement type-safe dependency injection in TypeScript?
  2. What is the significance of never and unknown in TypeScript, and how do you use them effectively?
  3. What are advanced patterns of generics, such as "generics with constraints" or "recursive generics" in TypeScript?
  4. How does TypeScript handle and generate declaration files (.d.ts), and why are they important?
  5. How would you optimize a TypeScript application in terms of performance and type-checking?
  6. What is the role of "type guards" in TypeScript, and how do they enhance type safety?
  7. How do you use conditional types for more complex type relationships in TypeScript?
  8. How do you implement and enforce "strict null checks" in TypeScript, and why is it important?
  9. What is the as const assertion in TypeScript, and how does it help in defining literal types?
  10. How do you handle external JavaScript libraries that don't have type definitions in TypeScript?
  11. Explain the concept and use of advanced TypeScript utility types such as ReturnType, Parameters, and InstanceType.
  12. How do you create a type-safe event bus or observer pattern in TypeScript?
  13. How does TypeScript work with the this context, and what are common pitfalls to avoid?
  14. What is the significance of "type inference" when using TypeScript generics?
  15. What is the purpose of the unknown type, and how is it different from any?
  16. How do you design an abstract class in TypeScript that enforces specific contracts while allowing flexibility in implementation?
  17. How do you handle multiple types in a single function signature (overloading) in TypeScript?
  18. How would you implement a type-safe configuration management system in TypeScript?
  19. What are the best practices for organizing large TypeScript projects?
  20. What is the infer keyword, and how can it be used in conditional types to infer types?
  21. How do you leverage TypeScript’s type system to create reusable, composable components in React or other frameworks?
  22. What is the role of "module augmentation" in TypeScript, and how would you extend a third-party module’s type definitions?
  23. How does TypeScript's lib.d.ts file work, and how does it impact the development experience?
  24. What is the difference between the typeof operator and the keyof operator in TypeScript, and when would you use each?
  25. How do you handle "type erasure" in TypeScript, and how does it affect generics?
  26. What is a "recursive type" in TypeScript, and when would you use it?
  27. What are "mapped types," and how can they be used to transform types in TypeScript?
  28. How does TypeScript work with decorators, and what are some best practices?
  29. How do you handle type-safe API responses using TypeScript?
  30. What are the trade-offs between using "any" vs. "unknown" in TypeScript for complex applications?
  31. How do you implement and enforce custom validation or transformation logic in TypeScript types?
  32. What is the importance of the ReadonlyArray<T> type in TypeScript, and how does it differ from a regular array?
  33. How do you create a type-safe redux-like state management system in TypeScript?
  34. What are the challenges of working with dynamic types or dynamic imports in TypeScript?
  35. How do you work with TypeScript's declaration merging to extend existing types or interfaces?
  36. How would you design a type-safe API that can return different data types based on input parameters in TypeScript?
  37. How do you use type inference with generics and conditional types to create dynamic, adaptable components in TypeScript?
  38. What is the purpose of as and const assertions in TypeScript, and how do they affect type narrowing?
  39. How do you ensure the scalability and maintainability of a TypeScript codebase in large-scale applications?
  40. What are some advanced debugging and troubleshooting strategies for TypeScript in a production environment?

Typescript Interview Questions and Answers

Beginners (Q&A)

1. What is TypeScript, and how does it differ from JavaScript?

TypeScript is a strongly typed superset of JavaScript that adds optional static typing, interfaces, and other advanced features to the language. Developed and maintained by Microsoft, TypeScript compiles down to standard JavaScript, which can then run anywhere JavaScript is supported, such as in browsers or Node.js.

Key Differences between TypeScript and JavaScript:

  1. Static Typing: TypeScript introduces a static type system, which allows developers to define the types of variables, function parameters, and return values. This type checking is performed at compile time, which helps catch errors early in the development process. JavaScript, on the other hand, is a dynamically typed language where types are only determined at runtime, which increases the likelihood of runtime errors.
  2. Type Inference: While TypeScript allows you to explicitly define types, it also has a powerful type inference mechanism. This means TypeScript can automatically determine types for variables and expressions based on the values assigned, reducing the need for explicit type annotations in many cases.
  3. Interfaces and Classes: TypeScript provides more robust support for object-oriented programming, introducing features like interfaces, abstract classes, and access modifiers (e.g., public, private, protected), which aren’t present in JavaScript in the same way. This makes TypeScript better suited for large-scale applications and teams working on complex codebases.
  4. Compilation: TypeScript is a compiled language, meaning the code you write in TypeScript must be compiled into JavaScript before it can run. This step provides an opportunity to catch errors and perform checks before the code is executed. JavaScript, on the other hand, is an interpreted language, running directly in browsers or Node.js environments.
  5. Tooling: TypeScript offers richer tooling support compared to JavaScript, thanks to its static typing. Many IDEs (e.g., Visual Studio Code) offer features like autocompletion, real-time error checking, and refactoring assistance, improving the development experience.

In summary, while TypeScript is a superset of JavaScript that provides additional features, it allows developers to write more reliable and maintainable code, especially in large, complex projects, and ensures that errors related to types are caught early in the development lifecycle.

2. What are the benefits of using TypeScript over JavaScript?

Using TypeScript over JavaScript offers several substantial benefits, particularly in larger codebases or when working in teams. Here are some of the key advantages:

  1. Type Safety and Early Error Detection: TypeScript's static typing system ensures that type errors are caught at compile time, preventing many runtime errors. By explicitly defining types for variables, function parameters, and return values, you reduce the likelihood of bugs related to type mismatches. For example, trying to add a string and a number in TypeScript will throw an error at compile time, whereas JavaScript will allow the operation but might yield unintended results.
  2. Improved Developer Productivity: TypeScript provides strong tooling support. IDEs like Visual Studio Code offer autocompletion, inline type checking, and automatic error detection. This improves productivity by catching errors early in the development process and reducing debugging time. It also helps developers work faster by offering context-aware suggestions and helping them understand the code more easily.
  3. Enhanced Maintainability: Static typing makes the code more maintainable by providing better documentation of what data types are expected. This is especially important in large codebases, where multiple developers work together. Having clear types also makes it easier to refactor code because you can rely on TypeScript’s compiler to catch any mismatches or incorrect changes.
  4. Support for Modern JavaScript Features: TypeScript supports modern JavaScript features (like async/await, destructuring, and spread operators) and compiles them down to an older version of JavaScript (e.g., ES5) for compatibility with legacy browsers or runtimes. This means you can write modern JavaScript code while ensuring broad compatibility.
  5. Object-Oriented Programming Support: TypeScript enhances JavaScript’s object-oriented features by adding things like interfaces, classes, and inheritance with stronger type support. TypeScript also supports abstract classes, generics, and access modifiers, which JavaScript lacks in its native form.
  6. Better Tooling for Large Teams: TypeScript is especially useful in large-scale projects or teams. Its ability to define contracts between modules via interfaces, along with tools like static code analysis and linters, helps keep the codebase consistent and easier to navigate. This can help prevent common issues in large teams, such as miscommunication or incorrect assumptions about the behavior of modules.
  7. Type Definitions for External Libraries: TypeScript offers the ability to use Type Definitions (via the DefinitelyTyped project or @types package) for JavaScript libraries. This provides type safety when working with external libraries, which may not have been written with TypeScript in mind.

3. Explain the concept of "type annotations" in TypeScript.

Type annotations in TypeScript refer to the process of explicitly specifying the types of variables, function parameters, return values, and other data structures. By providing type annotations, you help TypeScript’s type checker understand the expected types in your code, which in turn allows TypeScript to catch potential errors during development before the code is executed.

For example:

let age: number = 25;
let name: string = "Alice";
let isActive: boolean = true;

In the above example:

  • age: number indicates that age must always hold a value of type number.
  • name: string indicates that name must always hold a string value.
  • isActive: boolean indicates that isActive must always be a boolean.

Function Annotations: Type annotations are also used when defining functions. You can specify the types of parameters and the return type of the function:

function greet(name: string): string {
    return `Hello, ${name}`;
}

In this case, name: string specifies that the greet function expects a string as an argument, and the : string after the parentheses specifies that the function returns a string.

Benefits of Type Annotations:

  • Early Error Detection: Type annotations allow TypeScript to catch type-related errors at compile time, preventing runtime issues.
  • Better Code Readability: Annotations act as self-documenting code, making it easier for developers (especially those new to the codebase) to understand the expected types of variables and functions.
  • Refactoring Assistance: With explicit types, tools like IDEs and linters can help safely refactor the code without introducing type mismatches.

4. How do you declare a variable in TypeScript with a specific type?

In TypeScript, you declare a variable with a specific type by using the : syntax followed by the type. TypeScript uses type annotations to ensure that variables hold values of the correct type.

Here’s the syntax for declaring a variable with a type:

let variableName: type = value;

Example:

let num: number = 10;      // Declares a variable 'num' with type 'number'
let name: string = "Alice"; // Declares a variable 'name' with type 'string'
let isActive: boolean = true; // Declares a variable 'isActive' with type 'boolean'

Type annotations can also be used with complex data types:

Arrays:

let numbers: number[] = [1, 2, 3];
let names: Array<string> = ["Alice", "Bob"];

Objects:

let user: { name: string, age: number } = { name: "Alice", age: 30 };

Tuples:

let tuple: [number, string] = [1, "Alice"];

By declaring the types explicitly, TypeScript can check that variables hold the expected types, helping catch errors at compile time.

5. What is the any type in TypeScript, and when should it be used?

The any type in TypeScript is a dynamic type that can represent any value, essentially opting out of TypeScript's strict type system for that variable. When you assign the any type to a variable, TypeScript allows that variable to hold any kind of data — be it a number, string, object, function, or even null.

Example:

let value: any = 5;
value = "Hello, TypeScript";  // No error, as 'value' can hold any type
value = { name: "Alice" };    // Still valid

When to Use any:

  • Prototyping or Temporary Code: During rapid prototyping or experimentation, you may not want to commit to specific types until the logic is clearer. The any type can be useful in these situations, though it should be avoided once the prototype turns into production-level code.
  • Third-Party Libraries Without Type Definitions: When working with third-party libraries that do not provide TypeScript type definitions, any can be used temporarily to avoid type errors. However, it’s better to use TypeScript’s declaration files or write custom typings for such libraries.
  • Dynamic Data: In cases where the type of a variable is truly dynamic and cannot be inferred or strictly defined, using any allows flexibility. For example, when parsing JSON data from an external source where the structure is unknown ahead of time.

However, any should be used sparingly. Overusing it undermines the benefits of TypeScript’s type safety, making the code more prone to errors. It's generally recommended to use unknown when you need flexibility but still want to enforce type checking before using the value.

6. How do you define a function in TypeScript with specific argument and return types?

In TypeScript, you can define a function with specific argument types and a return type by annotating the types of the parameters and the return type after the function signature. Here’s the general syntax:

typescript

function functionName(parameterName: type): returnType {
  // Function body
}

Example:

function add(a: number, b: number): number {
  return a + b;
}

console.log(add(5, 10)); // Outputs: 15

In this example:

  • The function add takes two arguments, a and b, both of type number.
  • The return type of the function is also number, ensuring that the function will always return a value of type number.

You can also define functions with optional or default parameters:

function greet(name: string, age?: number): string {
  return `Hello ${name}, you are ${age ? age : 'unknown'} years old.`;
}

console.log(greet("Alice")); // Outputs: Hello Alice, you are unknown years old.

7. What is type inference in TypeScript?

Type inference in TypeScript refers to the compiler's ability to automatically deduce the type of a variable or expression based on its assigned value, without needing explicit type annotations. TypeScript uses inference to determine the type of variables, function return types, and other expressions at compile time.

Example:

let num = 10; // TypeScript infers that 'num' is of type 'number'
let greeting = "Hello"; // TypeScript infers that 'greeting' is of type 'string'

In the above example, TypeScript automatically infers that num is a number and greeting is a string, based on their assigned values.

Type inference helps reduce the verbosity of TypeScript code by eliminating the need for explicit type annotations in many cases. However, you can always override inferred types by using type annotations.

8. Explain the difference between let, const, and var in TypeScript.

In TypeScript (and JavaScript), there are three ways to declare variables: let, const, and var. These keywords differ in how they handle scoping, mutability, and hoisting.

  1. let:
    • Scope: Block-scoped (limited to the block, statement, or expression where it is used).
    • Mutability: Variables declared with let can be reassigned to new values.
    • Hoisting: Variables declared with let are hoisted to the top of their block, but are not initialized until their actual declaration, leading to a "temporal dead zone" where the variable cannot be accessed before its declaration.
let name = "Alice";
name = "Bob"; // Reassignment is allowed


const:

  • Scope: Block-scoped (same as let).
  • Mutability: Variables declared with const cannot be reassigned after initialization.
  • Hoisting: Like let, const variables are hoisted, but are also not initialized until their declaration.
const pi = 3.14;
// pi = 3.14159; // Error: Cannot reassign a constant variable


var:

  • Scope: Function-scoped (variables declared with var are scoped to the nearest function block, or globally if declared outside of any function).
  • Mutability: Variables declared with var can be reassigned.
  • Hoisting: var declarations are hoisted to the top of their scope and initialized with undefined.
var name = "Alice";
var name = "Bob"; // Redeclaration is allowed in the same scope

Recommendation: Always prefer let and const over var in TypeScript, as they provide better scoping and prevent potential bugs related to variable redeclaration.

9. What are TypeScript's primitive types?

TypeScript provides several built-in primitive types. These types represent the simplest form of data in the language and cannot be broken down further:

number: Represents both integer and floating-point numbers.

let num: number = 42;

string: Represents text, enclosed in single, double, or backticks for template literals.

let name: string = "Alice";

boolean: Represents a logical value, either true or false.

let isActive: boolean = true;

null: Represents the absence of a value.

let x: null = null;

undefined: Represents a variable that has been declared but not initialized.

let y: undefined = undefined;

symbol: A unique and immutable primitive value used to create anonymous and unique object properties.

let sym: symbol = Symbol("description");

bigint: A large integer type that can represent values larger than the maximum value of the number type.

let bigNum: bigint = 12345678901234567890n;


10. What is the difference between undefined and null in TypeScript?

Both undefined and null represent "empty" or "no value" in TypeScript, but they are used in different contexts:

undefined: This is the default value for uninitialized variables. It is assigned to a variable when it is declared but not given a value.

let x: number | undefined;
console.log(x); // undefined, as no value is assigned yet

null: Represents the intentional absence of any object value. null is often used when you want to explicitly indicate that a variable should not reference any object.

let person: { name: string } | null = null; // Person is explicitly set to 'null'

Key Differences:

  • undefined usually means a variable has been declared but not assigned a value.

null represents an object that is intentionally empty or missing.

11. How do you define an array in TypeScript?

In TypeScript, you can define arrays in a few different ways, each allowing you to specify the type of elements the array will hold.

1. Using the Array<T> syntax:

This is the generic form where you specify the type of elements the array will hold inside the angle brackets.

Example:

let numbers: Array<number> = [1, 2, 3, 4];
let strings: Array<string> = ["apple", "banana", "cherry"];

2. Using the type[] syntax:

This is a shorthand version where you specify the element type followed by square brackets.

Example:

let numbers: number[] = [1, 2, 3, 4];
let strings: string[] = ["apple", "banana", "cherry"];

Mixed-Type Arrays:

You can also define arrays that hold mixed types, using tuples or union types, depending on your use case:

let mixedArray: (string | number)[] = [1, "hello", 2, "world"];

Readonly Arrays:

If you want an array to be immutable (i.e., you don't want to modify it after it's created), you can define it as a readonly array:

let numbers: readonly number[] = [1, 2, 3, 4];

12. What is a tuple in TypeScript?

A tuple in TypeScript is an ordered collection of elements where each element can have a different type. Tuples are similar to arrays, but unlike arrays where all elements must be of the same type, tuples allow for heterogenous types (i.e., different types in the same collection).

Example:

let tuple: [number, string, boolean] = [1, "hello", true];

In this example:

  • The first element is a number.
  • The second element is a string.
  • The third element is a boolean.

Tuples also support optional elements, meaning you can define a tuple where some elements are optional:

let tuple: [number, string?, boolean?] = [1, "hello"];

Tuple with spread:

Tuples can also include a spread operator to allow for an arbitrary number of elements in the array:

let tuple: [string, ...number[]] = ["apple", 1, 2, 3];

13. What is an enum in TypeScript, and how is it different from a regular object?

An enum in TypeScript is a special "object" that allows you to define a set of named constants. Enums can be used to represent a collection of related values, making your code more readable and less error-prone.

Types of Enums:

  1. Numeric Enums (default):
    • The values in a numeric enum are assigned numeric values, starting from 0 by default.
enum Direction {
  Up,
  Down,
  Left,
  Right
}
console.log(Direction.Up); // Output: 0

  1. String Enums:
    • In string enums, each member must be initialized with a string value.
enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT"
}

Differences Between Enums and Objects:

  • Enums are more readable and can be used to represent a set of related constants. In the case of numeric enums, the values are automatically assigned, while string enums require explicit assignments.
  • Objects can hold any type of key-value pairs, but enums are more structured and provide better tooling and type safety.

Example:

const DirectionObject = {
  Up: 0,
  Down: 1,
  Left: 2,
  Right: 3,
};

While you can achieve similar functionality with objects, enums provide better support for reverse mapping, clarity, and integration with TypeScript’s type system.

14. How do you define an object type in TypeScript?

In TypeScript, you can define the type of an object using interface or type alias, but the simplest way is to define an inline object type directly in the code.

Using object type annotation:

let person: { name: string; age: number } = {
  name: "Alice",
  age: 30
};

Here, person is an object where:

  • name must be a string.
  • age must be a number.

You can also define objects with optional properties using ?:

let person: { name: string; age?: number } = { name: "Alice" };

In this case, age is optional.

Using interfaces for complex object types:

interface Person {
  name: string;
  age: number;
  address?: string; // Optional property
}

let person: Person = {
  name: "Bob",
  age: 25
};

15. What are the differences between TypeScript interfaces and types?

Both interfaces and type aliases in TypeScript can be used to define object shapes, function signatures, and other types. However, they have some differences in terms of usage, extensibility, and syntax.

Key Differences:

  1. Extensibility:
    • Interfaces can be extended using the extends keyword, making them more suitable for object-oriented designs and inheritance.
interface Shape {
  area: number;
}

interface Circle extends Shape {
  radius: number;
}



    • Type Aliases cannot use extends in the same way, but can be composed with intersection (&) and union (|) types.
type Shape = { area: number };
type Circle = Shape & { radius: number };

  1. Declaration Merging:
    • Interfaces support declaration merging, meaning if you declare an interface multiple times with the same name, TypeScript will merge them together.
interface Person {
  name: string;
}

interface Person {
  age: number;
}

const person: Person = { name: "Alice", age: 25 };



    • Type aliases do not support declaration merging.
  1. Use Cases:
    • Interfaces are generally preferred for defining object shapes, especially when you want to leverage inheritance or declaration merging.
    • Type aliases are often used for more complex types, including unions, intersections, or when defining primitive types, tuples, etc.

16. What is the void type in TypeScript?

The void type in TypeScript represents the absence of any value, typically used for functions that do not return a value. In JavaScript, functions can implicitly return undefined, but in TypeScript, you can explicitly declare a function’s return type as void.

Example:

function logMessage(message: string): void {
  console.log(message);
}

In this example, the function logMessage takes a string as an argument and does not return anything, so its return type is void.

The void type is also used when you expect a function to return nothing, indicating that the function is purely for its side effects (e.g., logging, updating state).

17. What does never mean in TypeScript, and when is it used?

The never type represents values that never occur. It is used

to define the return type of a function that will never successfully return a value, such as functions that always throw an error or enter an infinite loop.

Examples of never:

Functions that throw errors:

function throwError(message: string): never {
  throw new Error(message);
}

Infinite loops:

function infiniteLoop(): never {
  while (true) {
    console.log("Running...");
  }
}

The never type is useful when you want to ensure that a certain function cannot complete normally, providing better type safety.

18. How does TypeScript handle type compatibility?

TypeScript uses structural typing to handle type compatibility, meaning that two types are compatible if they have the same structure (i.e., the same properties with matching types), rather than relying on their names or definitions.

Example:

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

let employee: Person = { name: "Alice", age: 30 };

// Employee object is compatible with Person type because they have the same structure

TypeScript uses a duck typing approach, where the type compatibility is determined by the presence of properties and their types, not by the specific types' names. If two objects or types have the same shape, they are compatible regardless of whether they are explicitly declared as the same type.

19. What is a type alias, and how does it work in TypeScript?

A type alias in TypeScript allows you to create a custom name for any type. You can use type aliases to simplify complex types, provide clarity, or define reusable type definitions.

Example:

type ID = string | number;
let userId: ID = "abc123"; // Can be either a string or a number

Type aliases can be used to represent primitive types, objects, function types, and even unions and intersections.

Complex example:

type Point = { x: number, y: number };
type Point3D = Point & { z: number }; // Intersection type

let point: Point3D = { x: 1, y: 2, z: 3 };

Type aliases provide flexibility and are especially useful when working with complex types that are used repeatedly across your codebase.

20. How do you define an optional property in TypeScript?

In TypeScript, you can define an optional property in an object or interface by appending a question mark (?) to the property name.

Example:

interface Person {
  name: string;
  age?: number; // age is optional
}

let person1: Person = { name: "Alice" }; // Valid
let person2: Person = { name: "Bob", age: 30 }; // Also valid

The ? indicates that the property is not required, and objects of that type may or may not include that property. When the property is not provided, it is treated as undefined.

21. What is the purpose of the readonly modifier in TypeScript?

The readonly modifier in TypeScript is used to make properties or variables immutable, meaning their values cannot be changed once they are initialized. This can be applied to object properties, arrays, and class members.

1. readonly on object properties:

When applied to an object property, it prevents the property from being reassigned after it is initially set.

Example:

interface Person {
  readonly name: string;
  readonly age: number;
}

let person: Person = { name: "Alice", age: 25 };
person.name = "Bob";  // Error: Cannot assign to 'name' because it is a read-only property

2. readonly on arrays:

The readonly modifier can also be applied to arrays to prevent modification of the array elements.

Example:

let numbers: readonly number[] = [1, 2, 3];
numbers[0] = 4;  // Error: Index signature in type 'readonly number[]' only permits reading
numbers.push(5);  // Error: Property 'push' does not exist on type 'readonly number[]'

The readonly modifier is useful for enforcing immutability and creating more predictable, bug-resistant code, especially when dealing with data that should not change after it is created.

22. How do you define a function with optional parameters in TypeScript?

In TypeScript, you can define a function with optional parameters by appending a question mark (?) to the parameter's name in the function signature. This means that the parameter is not required when the function is called.

Example:

function greet(name: string, age?: number): string {
  return age ? `Hello ${name}, you are ${age} years old.` : `Hello ${name}, age is unknown.`;
}

console.log(greet("Alice")); // Hello Alice, age is unknown.
console.log(greet("Bob", 30)); // Hello Bob, you are 30 years old.

In this example, age is an optional parameter, and TypeScript will allow the function to be called with or without it. If not provided, age will be undefined inside the function.

23. How do you define default values for function parameters in TypeScript?

In TypeScript, you can define default values for function parameters by assigning the default value directly in the function signature. This allows you to provide a fallback value if no argument is passed for that parameter.

Example:

function greet(name: string, age: number = 30): string {
  return `Hello ${name}, you are ${age} years old.`;
}

console.log(greet("Alice")); // Hello Alice, you are 30 years old.
console.log(greet("Bob", 25)); // Hello Bob, you are 25 years old.

In this example, age has a default value of 30. If no value is passed for age when the function is called, TypeScript will use 30 as the default value.

24. What is the purpose of the as keyword in TypeScript?

The as keyword in TypeScript is used for type assertion, which tells the TypeScript compiler to treat a value as a specific type, overriding its inferred or existing type. It doesn’t perform any runtime type checking, it only affects TypeScript’s type system.

Example:

let someValue: unknown = "This is a string";

// Type assertion with 'as'
let stringLength: number = (someValue as string).length;

console.log(stringLength); // Outputs: 16

In this example:

  • someValue is of type unknown, meaning TypeScript cannot infer its type until you assert it.
  • By using as string, you assert that someValue should be treated as a string, allowing access to string-specific properties like length.

Difference between as and <type> (angle bracket syntax):

Both as and <type> can be used for type assertions, but the as syntax is preferred in TypeScript (especially in JSX, where angle brackets are already used for JSX tags).

let stringLength: number = (<string>someValue).length; // Less common in modern TypeScript

25. How do you use the typeof operator in TypeScript?

In TypeScript, the typeof operator is used to obtain the type of a variable or expression. It can be used in both expressions and type annotations.

1. typeof in expressions:

It works similarly to JavaScript to retrieve the type of a value.

let message = "Hello, TypeScript!";
console.log(typeof message); // Output: "string"

2. typeof in type annotations:

You can use typeof to create a type based on the type of an existing variable or expression.

let user = { name: "Alice", age: 25 };

// Using 'typeof' to create a type from the 'user' object
let anotherUser: typeof user = { name: "Bob", age: 30 };

In this example, the type of anotherUser will automatically match the structure of user.

26. What is a namespace in TypeScript, and how is it used?

A namespace in TypeScript is a way to group related code, such as functions, classes, or variables, under a common name to avoid name collisions. Namespaces were previously used for module systems in TypeScript, but with the introduction of ES6 modules, their use has become less common. However, namespaces can still be useful in certain scenarios, especially when organizing code in large applications.

Example:

namespace MathUtils {
  export function add(a: number, b: number): number {
    return a + b;
  }

  export function subtract(a: number, b: number): number {
    return a - b;
  }
}

console.log(MathUtils.add(5, 3)); // Outputs: 8

In this example:

  • The MathUtils namespace contains two functions: add and subtract.
  • The export keyword makes those functions accessible outside the namespace.

Namespaces provide a simple way to organize code in a global scope, but when using ES6 modules, it’s often better to rely on import/export for code organization.

27. How do you perform type checking in TypeScript?

TypeScript is statically typed, meaning that type checking is done at compile time. However, there are times when you might need to perform type checks at runtime as well. You can perform type checks using the typeof operator (for primitive types) or instanceof (for class instances).

1. Using typeof for primitive types:

let value: any = "Hello, world!";

if (typeof value === "string") {
  console.log("It's a string!");
} else {
  console.log("It's not a string.");
}

2. Using instanceof for class instances:

class Person {
  constructor(public name: string, public age: number) {}
}

let person = new Person("Alice", 25);

if (person instanceof Person) {
  console.log("It's a Person object!");
}

3. User-defined type guards:

For more complex types, you can create custom type guards to check if an object matches a specific type:

function isPerson(obj: any): obj is Person {
  return obj && obj.name && obj.age;
}

let someObject = { name: "Bob", age: 30 };

if (isPerson(someObject)) {
  console.log(`${someObject.name} is a person.`);
}

28. What is the difference between interface and class in TypeScript?

In TypeScript, interfaces and classes serve different purposes:

  1. Interface:
    • Describes the shape of an object or class (i.e., the properties and methods it should have).
    • Can be used for object structure, but cannot contain implementation.
    • Can be implemented by a class to ensure that the class has the required properties or methods.

Example:

interface Shape {
  area: number;
  calculateArea(): number;
}

class Circle implements Shape {
  constructor(public radius: number) {}
  area: number = 0;

  calculateArea(): number {
    this.area = Math.PI * this.radius * this.radius;
    return this.area;
  }
}

  1. Class:
    • Defines a blueprint for creating objects and can include properties and methods with actual implementations.
    • Can implement interfaces and extend other classes.

Example:

class Circle {
  constructor(public radius: number) {}

  calculateArea(): number {
    return Math.PI * this.radius * this.radius;
  }
}

  1. In this case, the class can have its own implementations, and can optionally implement an interface.

29. What are the different ways to import and export modules in TypeScript?

TypeScript supports several ways to import and export modules, which align with ES6 module syntax.

1. Named exports and imports:

Export: You can export multiple named items from a module.

export const pi = 3.14;
export function calculateArea(radius: number): number {
  return pi * radius * radius;
}

Import: You import specific members from a module using curly braces.

import { pi, calculateArea } from './math';
console.log(calculateArea(5)); // Uses imported function

2. Default export and import:

Export: You can export a single default value from a module.

export default class Circle {
  constructor(public radius: number) {}
}

Import: You import the default value directly without curly braces.

import Circle from './Circle';
let circle = new Circle(5);

30. Explain the concept of "declaration merging" in TypeScript.

Declaration merging is a feature in TypeScript that allows multiple declarations with the same name to be merged into a single definition. This is particularly useful for augmenting types and interfaces, allowing for flexible extension of existing code.

Example with interfaces:

interface Person {
  name: string;
}

interface Person {
  age: number;
}

let person: Person = { name: "Alice", age: 30 };

In this case, TypeScript merges the two Person interfaces into a single interface, effectively adding age to the original Person type.

Example with modules:

You can also augment modules using declaration merging:

// In a separate module
declare module 'myModule' {
  export function foo(): void;
}

// Later in the same or another module
declare module 'myModule' {
  export function bar(): void;
}

// Now 'myModule' has both 'foo' and 'bar' functions

This allows you to extend the functionality of existing modules or libraries without modifying their original source code.

31. How do you use strict mode in TypeScript?

In TypeScript, strict mode is a set of compiler options that enable a wide range of type-checking features to help catch potential errors and enforce better coding practices. It makes TypeScript's type system more rigorous by enabling a number of strict checks, such as disallowing null or undefined in certain places, requiring explicit types, and ensuring non-nullable types.

To enable strict mode, you need to add the following to your tsconfig.json file:

{
  "compilerOptions": {
    "strict": true
  }
}

When strict is set to true, TypeScript enables the following strict checks:

  • noImplicitAny: Disallows variables or parameters with an implicit any type.
  • strictNullChecks: Makes null and undefined distinct types from other types, preventing errors related to them.
  • noImplicitThis: Ensures that the value of this in a function is not implicitly any.
  • alwaysStrict: Ensures that every TypeScript file is in strict mode by adding "use strict" at the top of each file.

Strict mode is highly recommended for better type safety and to catch potential issues early in development.

32. What is the unknown type in TypeScript?

The unknown type in TypeScript is a safer alternative to any. It represents any value, but unlike any, you cannot perform operations on an unknown value until you have narrowed its type through some form of checking.

Example of using unknown:

let value: unknown = 42;

if (typeof value === "number") {
  console.log(value * 2); // Now it's safe to use 'value' as a number
} else {
  console.log("Not a number");
}

not know in advance, but you want to enforce type checking before using the value.

  • Key difference between any and unknown:
    • With any, you can perform any operation on the value, bypassing type checking.
    • With unknown, you must first check or narrow down the type before using the value, ensuring safer handling of potentially unsafe values.

33. What are type guards in TypeScript?

Type guards in TypeScript are functions or expressions that allow you to narrow the type of a variable at runtime. Type guards help the TypeScript compiler understand the type of a variable more precisely within a specific scope.

Examples of common type guards:

  1. Using typeof for primitive types:

function isString(value: unknown): value is string {
  return typeof value === "string";
}

let myValue: unknown = "Hello, TypeScript!";
if (isString(myValue)) {
  console.log(myValue.toUpperCase()); // Safe to call string methods
}

  1. Using instanceof for class instances:
class Dog {
  bark() {
    console.log("Woof!");
  }
}

let animal: unknown = new Dog();
if (animal instanceof Dog) {
  animal.bark(); // Safe to call Dog methods
}

  1. Custom type guards: You can define custom type guards using the value is Type syntax:
interface Cat {
  meow: boolean;
}

function isCat(value: unknown): value is Cat {
  return (value as Cat).meow !== undefined;
}

let animal: unknown = { meow: true };
if (isCat(animal)) {
  console.log(animal.meow); // Safe to use `meow` property
}

Type guards improve type safety by enabling the narrowing of types based on runtime checks.

34. What are union types, and how are they used in TypeScript?

A union type in TypeScript allows a variable to hold values of more than one type. It is denoted using the pipe (|) symbol between types.

Example:

let value: string | number;

value = "Hello, TypeScript";  // Valid
value = 42;                   // Valid
value = true;                 // Error: Type 'boolean' is not assignable to type 'string | number'

Union types are useful when a variable can have multiple possible types, such as in situations where a function might return different types depending on the input.

Example with functions:

function printId(id: string | number) {
  console.log(id);
}

printId("12345");  // Valid
printId(12345);    // Valid
printId(true);     // Error: Argument of type 'boolean' is not assignable to parameter of type 'string | number'

You can also combine union types with other type features like null or undefined, which can create more complex type structures.

35. How do you use the never type in a function that throws an error?

The never type is used in TypeScript to represent values that never occur, such as the return type of a function that always throws an error, or enters an infinite loop. When a function has a return type of never, TypeScript knows that the function will never successfully complete and return a value.

Example with a function that throws an error:

function throwError(message: string): never {
  throw new Error(message);
}

throwError("Something went wrong!");  // This function never returns.

The never type is useful for cases like:

  • Functions that always throw exceptions or errors.
  • Functions with infinite loops that never terminate.

36. What is a "literal type" in TypeScript, and how does it work?

A literal type in TypeScript refers to a specific, exact value of a given type. Literal types can be used to specify a variable or parameter that can only accept specific values, rather than a broader type.

Example with string literal types:

let direction: "up" | "down"; // `direction` can only be "up" or "down"
direction = "up";  // Valid
direction = "left"; // Error: Type '"left"' is not assignable to type '"up" | "down"'

Literal types can also be used with numbers and booleans:

let age: 18 | 21 | 30;
age = 18;  // Valid
age = 25;  // Error: Type '25' is not assignable to type '18 | 21 | 30'

Literal types are especially useful when you need to constrain values to a specific set of values, like in function parameters or enum-like structures.

37. How do you use the keyof keyword in TypeScript?

The keyof keyword in TypeScript is used to create a union type of the keys of a given type. It can be applied to an object type, and it returns a union type that includes the keys of that object.

Example:

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

type PersonKeys = keyof Person;  // "name" | "age"

let key: PersonKeys;
key = "name"; // Valid
key = "age";  // Valid
key = "address";  // Error: Type '"address"' is not assignable to type '"name" | "age"'

keyof is particularly useful when you want to reference or work with keys dynamically, and it allows for better type safety when accessing object properties.

38. How do you handle third-party JavaScript libraries in TypeScript?

In TypeScript, when using third-party JavaScript libraries, there are several ways to ensure type safety:

Install Type Definitions: Many JavaScript libraries come with TypeScript type definitions either built-in or through DefinitelyTyped. You can install these types via npm using @types scope.

npm install @types/jquery

Importing a JavaScript library without type definitions: If the library doesn't have type definitions, you can declare it as any or unknown to bypass TypeScript's type checking.

declare var myLibrary: any;

myLibrary.someFunction(); // No type checking, but code will compile

Creating Custom Type Definitions: If no type definitions are available, you can create your own type declarations in a .d.ts file.

// myLibrary.d.ts
declare module 'myLibrary' {
  export function someFunction(): void;
}

By using type definitions, you can get IntelliSense, type checking, and better tooling when working with JavaScript libraries in TypeScript.

39. What is the difference between declare and import in TypeScript?

declare is used to tell TypeScript about the existence of variables, functions, or modules that are defined outside the TypeScript code (such as in JavaScript libraries or external scripts). It is used in declaration files (.d.ts) and allows TypeScript to understand the types or interfaces without generating any code.Example:

declare const myGlobalVar: string;

import is used to bring in modules or specific exports from external TypeScript or JavaScript files. It is used for actual imports, and the TypeScript compiler will include the necessary code in the output.Example:

import { myFunction } from './myModule';

In summary, declare is used for declarations where TypeScript doesn't generate any code, and import is used for actual module loading.

40. How do you enable TypeScript in a JavaScript project?

To enable TypeScript in a JavaScript project, follow these steps:

Install TypeScript:

npm install --save-dev typescript

Create a tsconfig.json file: This configuration file tells TypeScript how to transpile your code. You can generate it automatically with the tsc --init command.

npx tsc --init

  1. Rename .js files to .ts or .tsx: TypeScript will only type-check .ts or .tsx files. You can gradually migrate by renaming files one by one.

Install type definitions: If you are using third-party libraries, you should install type definitions for them. For example:

npm install @types/react
  1. Update build tools: If you use build tools like Webpack, Babel, or others, make sure to configure them to handle TypeScript files.

Compile the code: Run the TypeScript compiler to transpile .ts files into .js:

npx tsc

By following these steps, you can begin using TypeScript in a JavaScript project.

Intermediate Questions and Answers

1. What is the Partial<T> utility type in TypeScript, and how is it used?

The Partial<T> utility type in TypeScript is used to create a new type where all properties of the type T are optional. It is commonly used when you want to represent an object where some of the properties might not be provided, such as when updating an existing object.

Example:

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

const updateUser = (user: User, updates: Partial<User>) => {
  return { ...user, ...updates };
};

const user = { name: "John", age: 30, email: "john@example.com" };

const updatedUser = updateUser(user, { age: 31 });
console.log(updatedUser); // { name: "John", age: 31, email: "john@example.com" }

In the above example, Partial<User> makes all properties of the User interface optional, allowing you to pass any subset of the properties to the updateUser function.

2. What is the Readonly<T> utility type in TypeScript, and how is it different from readonly properties?

The Readonly<T> utility type in TypeScript is used to make all properties of a type T immutable (i.e., readonly). This means that you cannot reassign values to properties once they are initialized.

Example:

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

const user: Readonly<User> = { name: "Alice", age: 25 };
user.age = 26; // Error: Cannot assign to 'age' because it is a read-only property

The key difference between Readonly<T> and readonly properties:

  • Readonly<T> is a utility type that applies immutability to all properties of the given type T.
  • readonly is used to make specific properties immutable in an object, typically when defining the properties of an interface or class.

Example of readonly in a class:

class User {
  readonly name: string;
  constructor(name: string) {
    this.name = name;
  }
}

const user = new User("Alice");
user.name = "Bob"; // Error: Cannot assign to 'name' because it is a read-only property

3. What is the Pick<T, K> utility type in TypeScript?

The Pick<T, K> utility type is used to create a new type by picking a subset of properties K from an existing type T. This is useful when you want to create a new type that only includes specific properties from an existing type.

Example:

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

type UserInfo = Pick<User, "name" | "email">;

const userInfo: UserInfo = { name: "Alice", email: "alice@example.com" };

In the above example, Pick<User, "name" | "email"> creates a new type that only includes the name and email properties from the User type.

4. How do you use generics in TypeScript to create reusable components?

Generics in TypeScript allow you to create reusable components or functions that work with any data type while preserving type safety. A generic type is defined using the angle bracket syntax (<T>), where T is a placeholder for a specific type.

Example:

function identity<T>(value: T): T {
  return value;
}

const num = identity(123);    // num has type number
const str = identity("hello"); // str has type string

In this example, the identity function is a generic function. It can accept and return any type T, which is determined based on the argument passed when the function is called.

Using generics in interfaces:

interface Box<T> {
  value: T;
}

const numberBox: Box<number> = { value: 123 };
const stringBox: Box<string> = { value: "Hello" };

Generics enable you to write functions and components that are flexible and type-safe, allowing them to work with multiple types without sacrificing type checking.

5. What is the extends keyword used for in TypeScript?

The extends keyword in TypeScript is used for two main purposes:

Extending a class: When creating a new class, you can use extends to inherit properties and methods from another class (just like in other object-oriented languages).Example:

class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a sound`);
  }
}

class Dog extends Animal {
  speak() {
    console.log(`${this.name} barks`);
  }
}

const dog = new Dog("Rex");
dog.speak(); // Outputs: Rex barks

Constrained Generics: The extends keyword is also used to constrain generic types, ensuring that a given type T must satisfy certain conditions (e.g., it must be a subclass of a specific class or have specific properties).Example:

function printLength<T extends { length: number }>(value: T): number {
  return value.length;
}

printLength("Hello");  // Works, because string has a length property
printLength([1, 2, 3]); // Works, because array has a length property
printLength(123);      // Error: number doesn't have a length property

6. How do you constrain a generic type in TypeScript?

You can constrain a generic type in TypeScript by using the extends keyword. This ensures that the type argument passed to a generic function or class must adhere to a certain structure or type.

Example:

function getProperty<T extends { name: string }>(obj: T): string {
  return obj.name;
}

const person = { name: "Alice", age: 25 };
console.log(getProperty(person));  // Works: "Alice"

In this example, the T type is constrained to objects that have a name property, so you can safely access obj.name.

You can also constrain a generic to a specific type, class, or interface:

function printDate<T extends Date>(date: T): void {
  console.log(date.toDateString());
}

printDate(new Date());  // Works
printDate("2022-01-01");  // Error: Type 'string' is not assignable to type 'Date'

7. What are "mapped types" in TypeScript, and how are they useful?

Mapped types in TypeScript are a way to create new types by transforming properties of an existing type. You can create new types by iterating over the properties of an existing type and modifying them (e.g., making them optional, readonly, etc.).

Example: Making properties optional with a mapped type:

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

type PartialUser = {
  [K in keyof User]?: User[K];
};

const user: PartialUser = { name: "Alice" };  // `age` is optional

In this example, PartialUser is a new type where all properties of User are optional. This is equivalent to using the Partial<T> utility type.

Example: Making properties readonly with a mapped type:

type ReadonlyUser = {
  readonly [K in keyof User]: User[K];
};

const user: ReadonlyUser = { name: "Alice", age: 25 };
user.age = 26;  // Error: Cannot assign to 'age' because it is a read-only property

Mapped types are useful for creating dynamic types, especially when you need to transform or derive new types based on existing ones.

8. How does TypeScript handle asynchronous code, and what are Promise<T> and async/await in TypeScript?

TypeScript handles asynchronous code similarly to JavaScript, using Promise<T> for asynchronous operations and async/await syntax for handling asynchronous code in a more readable and synchronous-looking way.

1. Promise<T>:

A Promise<T> is used to represent a value that will be available at some point in the future, where T is the type of the value the promise will resolve to.

function fetchData(): Promise<string> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Data fetched");
    }, 1000);
  });
}

fetchData().then(data => console.log(data));  // Outputs: "Data fetched"

2. async and await:

The async keyword is used to declare an asynchronous function, and await is used to pause the execution of the function until the promise is resolved.

async function getData(): Promise<string> {
  const data = await fetchData();
  console.log(data);  // Outputs: "Data fetched"
}

getData();

async/await provides a more concise and synchronous-like syntax for handling asynchronous code, making it easier to work with promises and avoid callback hell.

9. How do you use the Record<K, T> utility type in TypeScript?

The Record<K, T> utility type in TypeScript is used to create an object type with specific keys (K) and corresponding values of type T. It's especially useful when you want to create objects with specific keys that are mapped to values of a certain type.

Example:

type UserRoles = "admin" | "editor" | "viewer";

const roles: Record<UserRoles, string> = {
  admin: "Administrator",
  editor: "Editor",
  viewer: "Viewer",
};

In this example, Record<UserRoles, string> creates an object type where the keys are "admin", "editor", and "viewer", and the values are strings.

10. What is the purpose of the this keyword in TypeScript, and how does it differ from JavaScript?

In TypeScript, the this keyword behaves similarly to JavaScript, referring to the current context or object within which the function is being executed. However, TypeScript provides more explicit typing for this within classes and functions.

In a function:

In JavaScript, this depends on how the function is called, which can lead to bugs. In TypeScript, you can specify the type of this to avoid errors.

function greet(this: { name: string }) {
  console.log(`Hello, ${this.name}`);
}

const person = { name: "Alice", greet };
person.greet();  // Works fine, this refers to the person object

In a class:

class User {
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  greet() {
    console.log(`Hello, ${this.name}`);
  }
}

const user = new User("Bob");
user.greet();  // 'this' refers to the User instance

In TypeScript, you can also use this in interfaces and classes to ensure that the correct context is used when accessing properties or methods.

11. What is the difference between interface and abstract class in TypeScript?

Both interface and abstract class are used to define the structure of objects, but they serve different purposes and have different behaviors.

Key Differences:

  1. Purpose:
    • interface: Defines a contract for the structure of objects, classes, or function signatures. It does not provide any implementation, only the method and property signatures.
    • abstract class: Can define both abstract methods (without implementation) and concrete methods (with implementation). It allows you to define common functionality that will be shared by derived classes.
  2. Implementation:
    • interface: A class or object that implements an interface must implement all its properties and methods. It cannot provide any implementation.
    • abstract class: A class that extends an abstract class must implement all abstract methods, but it can inherit implementations of concrete methods.
  3. Inheritance:
    • interface: Can be extended by multiple interfaces or classes.
    • abstract class: Can be extended by only one class (since classes support single inheritance).
  4. Instance Creation:
    • interface: Cannot be instantiated directly. It is used to define the structure of an object.
    • abstract class: Cannot be instantiated directly, but can provide common functionality that can be inherited by derived classes.

Example:

interface Shape {
  area: number;
  calculateArea(): void;
}

abstract class ShapeBase {
  abstract area: number;
  abstract calculateArea(): void;

  displayArea() {
    console.log(`Area: ${this.area}`);
  }
}

class Circle extends ShapeBase {
  area: number = 0;

  calculateArea() {
    this.area = Math.PI * 2 * 2; // Example for radius 2
  }
}

const circle = new Circle();
circle.calculateArea();
circle.displayArea(); // Output: Area: 12.566370614359172

12. How do you implement inheritance in TypeScript?

Inheritance in TypeScript is implemented using the extends keyword. A derived class inherits properties and methods from a base class.

Example:

class Animal {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a sound`);
  }
}

class Dog extends Animal {
  constructor(name: string) {
    super(name); // Call the base class constructor
  }

  speak() {
    console.log(`${this.name} barks`);
  }
}

const dog = new Dog("Rex");
dog.speak(); // Output: Rex barks

Here, Dog extends Animal, and the speak method is overridden to provide a different implementation for dogs.

13. What is the difference between an "interface" and a "type alias" in TypeScript?

Both interface and type alias can be used to define the structure of objects, but they differ in syntax, capabilities, and usage.

Key Differences:

  1. Extending and Implementing:
    • interface: Can be extended using extends and implemented using implements. It is typically used for defining object shapes and contracts.
    • type alias: Can define object types, union types, and intersection types, but it cannot be used with implements. It can be used for more complex types like unions or intersections.
  2. Declaration Merging:
    • interface: Supports declaration merging, which means you can declare the same interface multiple times, and TypeScript will automatically merge their definitions.
    • type alias: Does not support declaration merging. If you try to declare a type alias with the same name multiple times, you'll get an error.
  3. Complex Types:
    • interface: Generally used to describe objects or function signatures.
    • type alias: Can describe more complex types like union types, intersections, and even primitive types.

Example:

// Interface
interface Shape {
  width: number;
  height: number;
}

// Type Alias
type ShapeType = {
  width: number;
  height: number;
  color: string;
};

const shape1: Shape = { width: 10, height: 20 };
const shape2: ShapeType = { width: 10, height: 20, color: "red" };

14. What is the significance of the super keyword in TypeScript classes?

The super keyword is used in TypeScript to refer to the base (parent) class and is particularly useful when calling methods or accessing properties from the parent class.

Uses of super:

  1. Calling a parent class constructor: In a derived class, super() is used to call the constructor of the parent class.
  2. Accessing methods: You can call methods of the parent class using super.methodName().

Example:

class Animal {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a sound`);
  }
}

class Dog extends Animal {
  constructor(name: string) {
    super(name); // Call the base class constructor
  }

  speak() {
    super.speak(); // Call the base class speak method
    console.log(`${this.name} barks`);
  }
}

const dog = new Dog("Buddy");
dog.speak();
// Output: Buddy makes a sound
//         Buddy barks

15. How do you use the constructor in TypeScript classes?

In TypeScript, the constructor method is used to initialize a newly created instance of a class. It is called automatically when an object is instantiated.

Example:

class Person {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  greet() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
  }
}

const person = new Person("Alice", 25);
person.greet(); // Output: Hello, my name is Alice and I am 25 years old.

The constructor initializes the name and age properties when a new Person object is created.

16. What are "mixins" in TypeScript, and how do they work?

Mixins in TypeScript are a pattern for combining multiple classes or objects into one, allowing you to share functionality across different classes. This allows you to apply multiple behaviors to a single class, especially when single inheritance is limiting.

Example:

class CanSwim {
  swim() {
    console.log("Swimming!");
  }
}

class CanFly {
  fly() {
    console.log("Flying!");
  }
}

class Duck implements CanSwim, CanFly {
  swim: () => void;
  fly: () => void;
}

Object.assign(Duck.prototype, CanSwim.prototype, CanFly.prototype);

const duck = new Duck();
duck.swim(); // Output: Swimming!
duck.fly();  // Output: Flying!

In this example, the Duck class can both swim and fly because it mixes the behaviors of CanSwim and CanFly.

17. How do you declare a read-only property in a class in TypeScript?

You can declare a read-only property in a class using the readonly modifier. Once set, a readonly property cannot be modified outside of the constructor.

Example:

class Person {
  readonly name: string;
  readonly age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

const person = new Person("Alice", 30);
person.name = "Bob"; // Error: Cannot assign to 'name' because it is a read-only property

In this example, name and age are read-only properties, meaning they cannot be reassigned after the object is constructed.

18. How do you create and use custom decorators in TypeScript?

Custom decorators in TypeScript allow you to add behavior to classes, methods, or properties. They are functions prefixed with @ and can be used for various purposes, such as logging, validation, or metadata handling.

Example: Creating a Method Decorator

function log(target: any, key: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function(...args: any[]) {
    console.log(`Calling method ${key} with args: ${args}`);
    return originalMethod.apply(this, args);
  };
}

class Person {
  @log
  sayHello(name: string) {
    console.log(`Hello, ${name}!`);
  }
}

const person = new Person();
person.sayHello("Alice"); // Logs: Calling method sayHello with args: [ 'Alice' ]

In this example, the log decorator intercepts the method call and logs the arguments before calling the original method.

19. What is the infer keyword in TypeScript, and how does it work?

The infer keyword in TypeScript is used within conditional types to infer a type within the true branch of the condition. It's often used in type manipulation to extract types from other types dynamically.

Example:

type ReturnTypeOfFunction<T> = T extends (...args: any[]) => infer R ? R : never;

function getName() {
  return "Alice";
}

type NameType = ReturnTypeOfFunction<typeof getName>; // Type is 'string'

In this example, ReturnTypeOfFunction uses infer R to extract the return type of a function and use it elsewhere in the code.

20. What is a conditional type in TypeScript?

A conditional type in TypeScript allows you to define types based on a condition. It has the syntax T extends U ? X : Y, where:

  • T extends U: Checks if type T extends type U.
  • X: The type if the condition is true.
  • Y: The type if the condition is false.

Example:

type IsString<T> = T extends string ? "Yes" : "No";

type Test1 = IsString<string>;  // "Yes"
type Test2 = IsString<number>;  // "No"

In this example, IsString is a conditional type that returns "Yes" if T is a string, and "No" otherwise.

21. How does TypeScript support JSX/TSX?

TypeScript supports JSX/TSX (the syntax used in React) with full type-checking capabilities. When writing React code in TypeScript, you use .tsx files instead of .ts files to denote that the file contains JSX syntax.

Key features:

  1. Type Checking: TypeScript can check types in JSX/TSX components just like regular TypeScript code. This helps with ensuring props, state, and context are used correctly in React components.
  2. JSX Intrinsic Elements: TypeScript has built-in types for standard HTML elements and React components, allowing for autocompletion and type checking when rendering JSX elements.
  3. Integration with React: TypeScript works seamlessly with React’s types and props. You can define types for props and state, as well as use advanced features like generics and type inference within React components.

Example:

import React from "react";

interface ButtonProps {
  label: string;
}

const Button: React.FC<ButtonProps> = ({ label }) => {
  return <button>{label}</button>;
};

export default Button;

In this example, TypeScript ensures that the label prop is always a string when used within the Button component.

22. How do you perform type assertions in TypeScript?

Type assertions in TypeScript allow you to tell the compiler to treat a variable as a different type. Type assertions do not perform any runtime checks—they are purely a compile-time construct.

There are two ways to perform type assertions:

  1. Angle Bracket Syntax: (<Type>)value
  2. as Syntax: value as Type

Example:

let someValue: unknown = "Hello, TypeScript";

// Using angle bracket syntax
let strLength1: number = (<string>someValue).length;

// Using 'as' syntax
let strLength2: number = (someValue as string).length;

Both assertions above tell TypeScript to treat someValue as a string, which allows you to safely access properties like .length.

23. What are never and any types, and when should you use them in TypeScript?

  1. never Type: The never type represents values that never occur. It's typically used for functions that throw errors or have infinite loops, where no value is returned.
    • Use cases: Functions that throw exceptions or enter infinite loops.
    • Example:
function throwError(message: string): never {
  throw new Error(message);
}

  1. any Type: The any type represents any value and disables type checking for that variable. It should be used sparingly, as it effectively turns off type safety.
    • Use cases: When you don't know the type ahead of time or need to handle a wide range of types.
    • Example:
let x: any = 5;
x = "Hello";  // No error
x = true;     // No error



    • When to use:
      • never: When you know that a value will never be returned (e.g., in a function that throws).
      • any: When you don’t need type checking, but try to avoid using any in favor of more specific types whenever possible to maintain type safety.

24. What is the difference between function and => function syntax in TypeScript?

The main difference between the traditional function declaration (function) and the arrow function (=>) lies in how they handle the this keyword.

  1. Traditional function:
    • In traditional functions, the value of this is dynamic. It depends on how the function is called.
function traditionalFunction() {
  console.log(this); // `this` is determined by how the function is called
}

  1. Arrow Function (=>):
    • In arrow functions, this is lexically bound—it takes the value of this from the surrounding context (the enclosing function or class).
const arrowFunction = () => {
  console.log(this); // `this` is inherited from the surrounding context
};

  • Arrow functions are generally used when you want to maintain the this context of the surrounding environment, for example, when using them as callbacks or event handlers in React.

25. How does TypeScript handle module resolution?

TypeScript handles module resolution using two strategies:

  1. Classic Resolution: The older, default method, used for backward compatibility. It looks for files with extensions like .ts, .js, .d.ts, or .json in the same directory or parent directories.
  2. Node Resolution: The most common strategy used when working with Node.js or modern JavaScript projects. It mimics how modules are resolved in Node.js.
    • Looks for modules in node_modules directories.
    • Supports relative and non-relative imports (e.g., import fs from "fs").

Example:

// tsconfig.json example for module resolution
{
  "compilerOptions": {
    "moduleResolution": "node",
    "baseUrl": "./src", // Set base directory for imports
    "paths": {
      "*": ["./*"] // Allows you to define custom path mappings
    }
  }
}

With module resolution, TypeScript ensures that the correct module is found based on how the module is imported and the configuration in tsconfig.json.

26. How do you use async/await with generics in TypeScript?

You can use async/await with generics in TypeScript in the same way you use them with any other type. The key is that the return type of an async function is always a Promise, so you can use generics to specify the type of the resolved value.

Example:

async function fetchData<T>(url: string): Promise<T> {
  const response = await fetch(url);
  const data = await response.json();
  return data as T; // Returning data of type T
}

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

fetchData<User>("https://api.example.com/user")
  .then(user => console.log(user.name)); // Type is User

In this example, the generic type T ensures that fetchData returns a Promise<User> when called with a URL that fetches user data.

27. What is the difference between type and interface when used with generics in TypeScript?

The main difference between type and interface when used with generics is their syntax and capabilities.

  • type can represent union types, intersection types, mapped types, and more, while interface is mostly used to define the shape of objects.
  • type can also alias more complex types such as function signatures or any union/intersection types.
  • interface is used to describe the structure of objects and can be merged with other interfaces (declaration merging).

Example:

// Interface with generics
interface Wrapper<T> {
  value: T;
}

// Type Alias with generics
type WrapperType<T> = { value: T };

// Both can be used to describe objects of type `T`
const wrapper1: Wrapper<number> = { value: 42 };
const wrapper2: WrapperType<number> = { value: 42 };

The key takeaway is that type is more versatile for complex types, while interface is better suited for object structures and can be extended or merged.

28. What is the purpose of the declare keyword in TypeScript?

The declare keyword in TypeScript is used to declare variables, classes, functions, or modules that are defined elsewhere, often in external libraries or in globally available code. This is particularly useful for adding type definitions for code that doesn't have them.

Use cases:

  1. Declaring global variables (e.g., from a script tag in HTML).
  2. Declaring modules or external types (e.g., from a third-party library).

Example:

declare var myGlobalVar: string;
declare function myExternalFunction(a: number): void;

myGlobalVar = "Hello"; // TypeScript knows it's a string
myExternalFunction(5);

In this example, declare tells TypeScript that myGlobalVar and myExternalFunction exist, even though they are not explicitly defined in the TypeScript code.

29. How do you use the asserts keyword in TypeScript?

The asserts keyword is used in TypeScript to narrow down types within a function. It allows you to assert that a condition is true, and if the assertion is correct, the type of the variable is refined.

Example:

function isString(value: any): asserts value is string {
  if (typeof value !== "string") {
    throw new Error("Not a string");
  }
}

let unknownValue: any = "Hello, TypeScript!";
isString(unknownValue); // Narrowed to string
console.log(unknownValue.toUpperCase()); // Safe to call methods on string

Here, the asserts value is string tells TypeScript that after the isString function call, value will be of type string, enabling type-safe access to string properties.

30. What is a "destructuring" assignment in TypeScript, and how does it work?

Destructuring allows you to unpack values from arrays or properties from objects into distinct variables. It’s a shorthand syntax for extracting values and is fully supported in TypeScript.

Example (Array Destructuring):

const numbers = [1, 2, 3];
const [a, b, c] = numbers;

console.log(a); // 1
console.log(b); // 2
console.log(c); // 3

Example (Object Destructuring):

const person = { name: "Alice", age: 25 };
const { name, age } = person;

console.log(name); // Alice
console.log(age);  // 25

Destructuring is useful for extracting specific values from arrays or objects in a concise and readable manner.

31. How do you define and use interfaces with optional properties in TypeScript?

In TypeScript, you can define interfaces with optional properties using the ? operator. This marks a property as optional, meaning it may or may not be provided when an object is created.

Example:

interface Person {
  name: string;
  age?: number; // 'age' is an optional property
}

const person1: Person = { name: "Alice" }; // valid, age is optional
const person2: Person = { name: "Bob", age: 25 }; // valid, age is provided

In this example, the age property is optional, so person1 does not need to include it, while person2 includes both properties.

32. How do you handle errors and exceptions in TypeScript?

Error handling in TypeScript is done using the try, catch, and finally statements, just like in JavaScript. TypeScript enhances error handling by allowing you to type the errors in the catch block.

Example:

function throwError(message: string): never {
  throw new Error(message);
}

try {
  throwError("Something went wrong!");
} catch (error) {
  if (error instanceof Error) {
    console.error(error.message); // Access the error message safely
  }
}

In this example:

  • throwError throws an error.
  • In the catch block, TypeScript ensures that the error is an instance of Error, which allows for safer handling (like accessing the message property).

33. What are modules and namespaces in TypeScript, and how do they differ?

  1. Modules:
    • Definition: Modules are a way to organize and encapsulate code in TypeScript. A module is any file that contains a import or export statement.
    • Usage: Use export to expose variables, functions, or classes, and import to bring them into other files.

Example:

// file1.ts
export const greet = (name: string) => `Hello, ${name}`;

// file2.ts
import { greet } from './file1';
console.log(greet("Alice"));

  1. Namespaces:
    • Definition: A namespace is a way to group related code in the same scope. It's used to organize code within a single global namespace. Unlike modules, namespaces don’t use import/export and are global by default.
    • Usage: Use namespace to group related logic and avoid polluting the global namespace.

Example:

namespace MyNamespace {
  export const greet = (name: string) => `Hello, ${name}`;
}

console.log(MyNamespace.greet("Alice"));

  1. Key Difference:
    • Modules use import/export and are file-based.
    • Namespaces are typically used for code that will be compiled into a single file and grouped under a common namespace.

34. How do you perform runtime validation of types in TypeScript?

TypeScript's type system is primarily used at compile time, and there is no built-in runtime type checking. However, you can implement runtime type validation using custom checks or libraries such as zod or io-ts.

Example using custom validation:

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

function isPerson(obj: any): obj is Person {
  return typeof obj.name === 'string' && typeof obj.age === 'number';
}

const obj = { name: "Alice", age: 30 };

if (isPerson(obj)) {
  console.log(`${obj.name} is ${obj.age} years old`);
} else {
  console.error("Invalid person object");
}

In this example, isPerson is a user-defined type guard that validates whether the object conforms to the Person interface.

35. How do you use the Exclude<T, U> utility type in TypeScript?

Exclude<T, U> is a TypeScript utility type that constructs a type by excluding from T all types that are assignable to U.

Example:

type A = string | number | boolean;
type B = string | boolean;

type C = Exclude<A, B>; // Excludes 'string' and 'boolean' from 'A', so C is 'number'

In this example, C will be of type number, because string and boolean are excluded from type A.

36. What is the NonNullable<T> utility type, and when should it be used?

NonNullable<T> is a TypeScript utility type that removes null and undefined from the type T. It ensures that a value cannot be null or undefined.

Example:

type MaybeString = string | null | undefined;
type NonNullableString = NonNullable<MaybeString>; // string

const value: NonNullableString = "Hello"; // Valid
// const invalid: NonNullableString = null; // Error: Type 'null' is not assignable to type 'string'.

When to use:

  • Use NonNullable<T> when you want to ensure that a variable is neither null nor undefined, which is common when working with nullable data (like API responses).

37. How do you avoid common pitfalls in TypeScript type compatibility?

To avoid common pitfalls in TypeScript type compatibility, keep the following principles in mind:

  1. Explicit Types: Always define clear and explicit types, especially for function arguments and return values.
  2. Avoid any: Avoid using any, as it disables type checking. Instead, use unknown if you need a more strict version.
  3. Use Type Guards: Use type guards (typeof, instanceof, or custom guards) to safely narrow types.
  4. Union Types: Be cautious when using union types; ensure that type checks are in place to handle each type within the union.
  5. Strict Mode: Enable strict mode in your tsconfig.json to enforce stricter type checking and catch potential issues early.
{
  "compilerOptions": {
    "strict": true
  }
}

By enabling strict mode, TypeScript will enforce stricter checks, helping to prevent subtle bugs.

38. What is the purpose of the void type in TypeScript, and when is it used?

The void type in TypeScript is used to represent the absence of a value. It is commonly used as the return type for functions that do not return a value.

Example:

function logMessage(message: string): void {
  console.log(message);
}

const result = logMessage("Hello, TypeScript!");
// result is of type 'void', so you cannot use it further

When to use:

  • Use void when you have functions that don't return anything meaningful (e.g., logging, event handling, or setting state).

39. How do you create a type-safe event system in TypeScript?

To create a type-safe event system in TypeScript, you can use interfaces and generics to define events and listeners with strongly typed parameters.

Example:

type EventListener<T> = (event: T) => void;

interface MyEvent {
  type: "click";
  x: number;
  y: number;
}

class EventEmitter {
  private listeners: Map<string, EventListener<any>[]> = new Map();

  on<T>(eventType: string, listener: EventListener<T>) {
    const listeners = this.listeners.get(eventType) || [];
    listeners.push(listener);
    this.listeners.set(eventType, listeners);
  }

  emit<T>(eventType: string, event: T) {
    const listeners = this.listeners.get(eventType);
    if (listeners) {
      listeners.forEach(listener => listener(event));
    }
  }
}

const emitter = new EventEmitter();

// Register a listener for 'click' event
emitter.on<MyEvent>("click", (event) => {
  console.log(`Clicked at (${event.x}, ${event.y})`);
});

// Emit a 'click' event
emitter.emit("click", { type: "click", x: 100, y: 200 });

In this example, TypeScript ensures that event listeners and emitted events are type-safe, reducing the likelihood of runtime errors.

40. What is the difference between typeof and keyof in TypeScript?

typeof: The typeof operator is used to obtain the type of a variable or expression at compile time.Example:

const x = 42;
type XType = typeof x; // XType is 'number'

keyof: The keyof operator is used to obtain the union type of the keys (property names) of an object type.Example:

interface Person {
  name: string;
  age: number;
}
type PersonKeys = keyof Person; // PersonKeys is 'name' | 'age'

Summary:

  • typeof works at the value level to get the type of a variable.
  • keyof works at the type level to get the union of property names (keys) of an object type.

Experienced Questions and Answers

1. How do you implement type-safe dependency injection in TypeScript?

Type-safe dependency injection (DI) in TypeScript involves using classes, interfaces, and generics to ensure that dependencies are injected in a way that preserves type safety throughout your application.

Basic Implementation:

You can use TypeScript’s classes and interfaces to define services that are injected via a constructor, along with generics to ensure types are consistent.

Example:

interface Service {
  doSomething(): void;
}

class MyService implements Service {
  doSomething() {
    console.log('Service is doing something');
  }
}

class Consumer {
  private service: Service;

  constructor(service: Service) {
    this.service = service;
  }

  useService() {
    this.service.doSomething();
  }
}

const myService = new MyService();
const consumer = new Consumer(myService);
consumer.useService();  // Output: Service is doing something

Using Generics for More Flexibility:

You can extend this with generics to create a more flexible DI system, ensuring the correct dependencies are injected.

class Injector {
  private services = new Map<string, any>();

  register<T>(name: string, service: T) {
    this.services.set(name, service);
  }

  resolve<T>(name: string): T {
    const service = this.services.get(name);
    if (!service) {
      throw new Error(`Service not found: ${name}`);
    }
    return service;
  }
}

const injector = new Injector();
injector.register("myService", new MyService());
const myServiceInstance = injector.resolve<MyService>("myService");
myServiceInstance.doSomething(); // Type-safe dependency injection

This ensures that dependencies are injected correctly with their respective types, offering type safety while resolving dependencies.

2. What is the significance of never and unknown in TypeScript, and how do you use them effectively?

Both never and unknown are special types in TypeScript, but they have different purposes:

  1. never Type:
    • Purpose: Represents values that never occur. It's used for functions that throw errors or enter infinite loops.
    • When to use: Use never to indicate that a function doesn't return a value, typically when throwing an error or running indefinitely.

Example:

function throwError(message: string): never {
  throw new Error(message);
}

  1. unknown Type:
    • Purpose: Represents any value, but unlike any, it requires type-checking before being used. This makes it safer than any because you must perform some kind of validation (type guard) before performing operations on it.
    • When to use: Use unknown when you're working with values whose type is not known, but you still want type-safety (compared to any).

Example:

let value: unknown = 42;

if (typeof value === 'number') {
  console.log(value.toFixed(2));  // Now it's safe to treat `value` as a number
}



    • Best practice: Always use unknown instead of any when you need to work with values of uncertain type, as it forces you to perform runtime checks, ensuring type safety.

3. What are advanced patterns of generics, such as "generics with constraints" or "recursive generics" in TypeScript?

  1. Generics with Constraints:
    • Purpose: You can constrain a generic type to ensure it extends a specific type, giving you more control over the kinds of values that can be passed into a generic function or class.

Example:

function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

const merged = merge({ name: "Alice" }, { age: 25 });

  1. In this example, T and U are constrained to be object, so the function only accepts objects as arguments.
  2. Recursive Generics:
    • Purpose: Recursive generics allow you to define types that refer to themselves, often used for complex data structures like trees or linked lists.

Example:

interface Node<T> {
  value: T;
  children: Node<T>[];
}

const tree: Node<number> = {
  value: 1,
  children: [
    { value: 2, children: [] },
    { value: 3, children: [{ value: 4, children: [] }] }
  ]
};

  1. Recursive generics are useful for representing hierarchical or nested data structures.

4. How does TypeScript handle and generate declaration files (.d.ts), and why are they important?

Declaration files (.d.ts) provide TypeScript with type information about JavaScript code, allowing TypeScript to check types in code that doesn't have built-in type definitions (e.g., third-party libraries, external JavaScript code).

  • Purpose: .d.ts files are used to declare the types of objects, functions, and variables available in an external library or script. They provide TypeScript with enough information to understand the types and perform type checking.
  • When to use:
    • External libraries: If you're working with JavaScript libraries that don’t have TypeScript definitions, you can create or find .d.ts files.
    • Global variables: Declare global types that exist outside the TypeScript module system.

Example:

// person.d.ts
declare module 'person' {
  export interface Person {
    name: string;
    age: number;
  }
  export function getPerson(): Person;
}

Why important:

  • .d.ts files allow TypeScript to perform type-checking on code written in JavaScript.
  • They're essential for working with third-party libraries that don’t include their own type definitions.

5. How would you optimize a TypeScript application in terms of performance and type-checking?

Here are some key strategies to optimize a TypeScript application:

Use strict mode: Enable strict type-checking options in the tsconfig.json to catch more errors at compile-time.

{
  "compilerOptions": {
    "strict": true
  }
}

  1. Optimize Type Inference: Minimize the use of any and unknown, and provide explicit type annotations when necessary to improve code readability and type inference accuracy.
  2. Reduce unnecessary type checks: Use conditional types and type guards efficiently to ensure that TypeScript doesn't perform unnecessary checks.
  3. Avoid large types in performance-critical paths: Types that are too large (e.g., deep object types) can slow down type checking. Use more concise types where possible.
  4. Optimize build configurations:
    • Enable incremental compilation to speed up successive builds.
    • Use the skipLibCheck option to avoid type checking of declaration files in node modules, speeding up the compilation process.
  5. Use const assertions: This can help in narrowing types (e.g., making arrays and objects readonly) at compile time, reducing the number of type checks.

6. What is the role of "type guards" in TypeScript, and how do they enhance type safety?

Type guards in TypeScript are functions or expressions that narrow the type of a variable within a specific scope, ensuring that operations on the variable are type-safe.

  • Role: Type guards help TypeScript understand the specific type of a variable at runtime. They allow you to write safer code by ensuring the correct type is used.
  • Examples of Type Guards:

typeof for primitive types:

function isString(value: unknown): value is string {
  return typeof value === 'string';
}

instanceof for class instances:

function isDate(value: unknown): value is Date {
  return value instanceof Date;
}

User-defined type guards for more complex checks:

function isPerson(obj: any): obj is Person {
  return obj && typeof obj.name === 'string' && typeof obj.age === 'number';
}

Type guards enhance type safety by ensuring that you can safely perform operations on variables once their type has been narrowed.

7. How do you use conditional types for more complex type relationships in TypeScript?

Conditional types in TypeScript are used to define types that depend on a condition. They follow the pattern:

T extends U ? X : Y

Where if type T extends type U, the type will be X; otherwise, it will be Y.

Example:

type IsString<T> = T extends string ? "Yes" : "No";

type Test1 = IsString<string>;  // "Yes"
type Test2 = IsString<number>;  // "No"

Advanced Example with Conditional Types:

type Flatten<T> = T extends (infer U)[] ? U : T;

type TestArray = Flatten<number[]>; // number
type TestString = Flatten<string>;  // string

Conditional types allow for more flexible and dynamic type definitions, useful for complex type transformations.

8. How do you implement and enforce "strict null checks" in TypeScript, and why is it important?

Strict null checks enforce that null and undefined are not assignable to any type except null and undefined, unless explicitly stated. This improves safety by preventing runtime errors where null or undefined values might be accessed incorrectly.

To enable strict null checks:

Set strictNullChecks to true in the tsconfig.json:

{
  "compilerOptions": {
    "strictNullChecks": true
  }
}

After enabling this, null and undefined will be treated as distinct types. For example:

let name: string = null;  // Error: Type 'null' is not assignable to type 'string'.

Why is it important:

  • It helps to catch bugs where null or undefined values may cause runtime issues.
  • It makes your code more predictable by ensuring that values are either explicitly null/undefined or have actual values.

9. What is the as const assertion in TypeScript, and how does it help in defining literal types?

The as const assertion in TypeScript is used to convert values into their most specific literal type, preventing them from being widened to broader types.

Example:

const colors = ["red", "green", "blue"] as const;
// colors is now of type ["red", "green", "blue"], not string[]

This allows you to define literal types more effectively:

type Colors = typeof colors[number]; // 'red' | 'green' | 'blue'

The as const assertion is useful when you want to treat a value as a specific constant type, rather than a broader type like string or number.

WeCP Team
Team @WeCP
WeCP is a leading talent assessment platform that helps companies streamline their recruitment and L&D process by evaluating candidates' skills through tailored assessments