Most Frequently asked typescript Interview Questions (2024)

author image Hirely
at 28 Dec, 2024

Question: What is TypeScript, and how is it different from JavaScript?

Answer:

TypeScript is a superset of JavaScript that adds static typing and other features to the language. It was developed by Microsoft to address some of the shortcomings of JavaScript, particularly when it comes to large-scale application development. TypeScript compiles down to regular JavaScript, which can run in any environment where JavaScript is supported (e.g., browsers, Node.js).

Here’s a detailed comparison of TypeScript and JavaScript:

Key Features of TypeScript:

  1. Static Typing:

    • TypeScript introduces type annotations, which allow you to specify the type of variables, function parameters, and return values. This enables type checking during development and can help catch type-related errors before the code runs.
    • Example:
      let name: string = "John";
      let age: number = 30;
    • JavaScript, on the other hand, is dynamically typed, which means variables do not have fixed types, and type-related errors are only detected at runtime.
  2. Type Inference:

    • TypeScript also supports type inference, meaning that it can automatically deduce the type of a variable based on its value, even if the type isn’t explicitly declared.
    • Example:
      let name = "John"; // inferred as type 'string'
      let age = 30; // inferred as type 'number'
    • In JavaScript, the type is inferred at runtime, but there’s no way to explicitly define or enforce types during development.
  3. Interfaces and Type Aliases:

    • TypeScript allows you to define interfaces and type aliases to describe the structure of objects, arrays, and functions, enabling better type safety and clarity.
    • Example of Interface:
      interface Person {
        name: string;
        age: number;
      }
    • This helps define contracts for objects, ensuring that they adhere to a specific structure.
  4. Classes and Object-Oriented Programming:

    • TypeScript improves upon JavaScript’s class syntax by introducing features like access modifiers (public, private, protected), abstract classes, and generics for more robust object-oriented programming.
    • Example:
      class Employee {
        private name: string;
        private role: string;
      
        constructor(name: string, role: string) {
          this.name = name;
          this.role = role;
        }
      
        getDetails() {
          return `${this.name} is a ${this.role}`;
        }
      }
    • While JavaScript supports classes, it does not include access modifiers or other advanced features for OOP.
  5. Generics:

    • TypeScript supports generics, which allow you to create reusable components or functions that work with multiple types while maintaining type safety.
    • Example:
      function identity<T>(arg: T): T {
        return arg;
      }
      let result = identity("Hello, World!"); // inferred as 'string'
    • JavaScript lacks built-in support for generics.
  6. Modern JavaScript Features:

    • TypeScript is a superset of JavaScript, so it supports all modern JavaScript features (ES6+), including async/await, destructuring, spread operator, modules, etc.
    • Additionally, TypeScript may include future JavaScript features even before they are officially supported in all browsers or JavaScript engines.
  7. Tooling Support:

    • TypeScript comes with strong tooling support, including:
      • Static code analysis to detect errors during development.
      • IntelliSense for code completion and better developer productivity.
      • Refactoring tools to improve maintainability.
    • Most modern IDEs (like Visual Studio Code) have excellent support for TypeScript, making it easier to catch errors early in the development process.

Differences Between TypeScript and JavaScript:

FeatureJavaScriptTypeScript
TypingDynamically typedStatically typed (with optional types)
CompilationInterpreted (directly executed)Compiles to JavaScript (via TypeScript compiler)
Type CheckingNo type checking (errors are runtime)Type checking at compile time
SyntaxECMAScript syntaxECMAScript syntax + additional type annotations
Development SpeedFaster development (less boilerplate)Slower initial development (due to types)
ToolingLimited tooling for type errorsAdvanced tooling (autocomplete, refactoring, error checking)
GenericsNot supportedSupported
Support for OOPClasses available, but no access modifiersClasses with access modifiers, abstract classes, etc.
Cross-Browser SupportSupported in all modern browsersCompiles to JavaScript, so compatible with any JS environment
Learning CurveEasier for JavaScript developersSteeper learning curve due to typing and OOP features

Example: TypeScript vs. JavaScript

JavaScript Example (No Type Checking):

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

let result = add(10, "5");  // Runtime error: NaN
console.log(result);

TypeScript Example (With Type Checking):

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

let result = add(10, "5");  // Compile-time error: Argument of type 'string' is not assignable to parameter of type 'number'.
console.log(result);

Why Use TypeScript?

  • Improved Maintainability: TypeScript’s static typing and tools like type inference and type checking help you write code that’s easier to understand, refactor, and maintain, especially in large codebases.
  • Better Tooling: With TypeScript, you get enhanced code editor features like auto-completion, inline documentation, and error detection, making development more efficient.
  • Large-scale Applications: For teams and projects where you need stricter code rules and consistency, TypeScript is especially beneficial as it can prevent many common runtime errors.

Conclusion:

  • TypeScript adds a layer of type safety to JavaScript, improving developer experience, reducing bugs, and enhancing productivity.
  • JavaScript is more flexible but lacks the compile-time error checking that TypeScript provides.
  • TypeScript is recommended for larger projects or teams that need to maintain a scalable codebase, whereas JavaScript might be sufficient for smaller or less complex projects where flexibility is more important than type safety.

Question: What are the main benefits of using TypeScript?

Answer:

TypeScript offers several key benefits over JavaScript, particularly when it comes to building large-scale applications. Below are the main advantages of using TypeScript:

1. Static Typing

  • Type Safety: TypeScript introduces static typing, which allows you to specify the types of variables, function parameters, and return values. This helps catch errors at compile-time rather than at runtime.
  • Error Prevention: By enforcing type checks, TypeScript can help prevent many common bugs such as passing incorrect types to functions or trying to perform operations on undefined or null values.
  • Better Code Understanding: Type annotations provide clarity about what kind of data is expected, making the code easier to understand and reason about.

Example:

let name: string = "John"; // Type is explicitly set to 'string'
name = 42;  // Compile-time error: Type 'number' is not assignable to type 'string'

2. Enhanced Developer Productivity

  • Auto-Completion and IntelliSense: Modern IDEs like Visual Studio Code offer features like IntelliSense (auto-completion) and hover tooltips, which can show you function signatures, types, and documentation as you write code.
  • Error Checking: TypeScript catches type errors during development, providing immediate feedback. This reduces the need for runtime debugging and leads to fewer errors making it into production.

Example: When using TypeScript in an editor like Visual Studio Code, you’ll get auto-suggestions as you type:

let user: { name: string, age: number } = { name: "Alice", age: 30 };
user.na // 'name' will be suggested, based on the declared type

3. Improved Code Maintainability

  • Refactoring Support: With TypeScript’s static typing, you can safely refactor your code, knowing that type checks will ensure the changes are consistent across the codebase. If you rename a function or change a variable type, TypeScript will help identify places that need updating.
  • Large Codebases: In large projects, TypeScript makes it easier to manage and understand the code. As your application grows, static typing ensures that you don’t miss type-related errors, which are more common in JavaScript when scaling up.

Example: If you change a function signature in TypeScript, all calls to that function will show an error if they don’t match the new signature.

4. Advanced Object-Oriented Programming (OOP) Support

  • Access Modifiers: TypeScript introduces access modifiers like public, private, and protected to control the visibility of class properties and methods. This helps enforce better encapsulation and data hiding.
  • Abstract Classes and Interfaces: TypeScript supports abstract classes and interfaces, allowing you to define the structure of objects and enforce consistency in your classes and objects. This improves the design and flexibility of the code.
  • Generics: TypeScript supports generics, allowing you to create reusable components, functions, and classes that work with a variety of types while maintaining type safety.

Example (Class with Access Modifiers):

class Employee {
  private name: string; // Cannot be accessed directly outside this class
  public role: string;
  protected salary: number; // Can be accessed by derived classes

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

  getSalary(): number {
    return this.salary;
  }
}

5. Type Inference

  • TypeScript has a powerful type inference system, which means you don’t always need to explicitly define types. TypeScript will automatically infer the type based on the assigned value. This helps maintain type safety while reducing the need for verbose type annotations.
  • Type inference makes code cleaner without sacrificing the benefits of static typing.

Example:

let name = "John";  // Inferred as 'string'
let age = 25; // Inferred as 'number'

6. Backward Compatibility with JavaScript

  • TypeScript is a superset of JavaScript, meaning that every valid JavaScript code is also valid TypeScript code. You can gradually adopt TypeScript in your JavaScript project by converting files to .ts or .tsx one by one.
  • You can mix TypeScript and JavaScript in the same project, making it easier to introduce TypeScript without a complete rewrite.

7. Rich Ecosystem and Tooling

  • TypeScript integrates well with modern build tools and frameworks (such as Webpack, Babel, and React). It has support in most JavaScript libraries and frameworks, which provides type definitions through DefinitelyTyped or directly from the library authors.
  • Type Definitions: For popular JavaScript libraries, TypeScript provides type definition files (.d.ts) that allow you to get type checking and IntelliSense for third-party libraries that don’t natively include TypeScript support.

Example: With React and TypeScript, you can use .tsx files to get type checking and support for JSX elements.

interface Props {
  name: string;
}

const Greeting: React.FC<Props> = ({ name }) => <h1>Hello, {name}!</h1>;

8. Cross-Platform and Cross-Browser Compatibility

  • TypeScript compiles down to JavaScript, which can run in any environment that supports JavaScript (e.g., browsers, Node.js, React Native).
  • TypeScript supports modern JavaScript features (e.g., async/await, destructuring), and during the compilation process, you can choose which JavaScript version (ES5, ES6, etc.) you want your code to be compatible with. This ensures compatibility across a wide range of browsers and platforms.

9. Better Collaboration and Documentation

  • TypeScript’s type annotations act as self-documenting code. By looking at the type definitions, other developers can understand the intended use and structure of a function or class without needing additional documentation.
  • It promotes better collaboration among team members by enforcing consistent data types and reducing misunderstandings or misuses of code.

Example:

// TypeScript function with typed parameters
function add(a: number, b: number): number {
  return a + b;
}

10. Future-Proof

  • TypeScript is often ahead of JavaScript in terms of new language features. The TypeScript team is quick to implement proposals from the ECMAScript specification, so you can use cutting-edge JavaScript features even if they haven’t been widely supported in browsers yet.
  • TypeScript allows you to use decorators, async/await, and other modern features even before they are fully available in standard JavaScript.

Conclusion:

TypeScript enhances JavaScript by adding static typing, better tooling, and modern programming features that make development more predictable, scalable, and maintainable. Its benefits are especially apparent in large-scale applications, team environments, and projects that require robust error handling and code maintainability. By enforcing strong typing and offering powerful tools, TypeScript helps reduce bugs, improve refactoring, and boost developer productivity.

Question: Can you explain what any, unknown, never, and void types are in TypeScript?

Answer:

In TypeScript, the types any, unknown, never, and void are special types that represent different categories of values or states in the type system. Here’s a detailed explanation of each:


1. any Type

The any type is the most flexible type in TypeScript. It allows a variable to be assigned any value, effectively bypassing TypeScript’s static type-checking.

  • Use Cases: any is often used when migrating from JavaScript to TypeScript or when you are unsure of the type at the time of writing the code.
  • Behavior: Any operation can be performed on a variable of type any without raising a type error. This defeats the purpose of static typing because it allows for any value, making type safety ineffective.
  • Recommendation: Use any sparingly as it can lead to less type safety and harder-to-maintain code.

Example:

let value: any = "Hello";
value = 42; // No error, type is dynamic
value = true; // No error, type is dynamic

2. unknown Type

The unknown type is similar to any in that it can hold any value, but it is safer than any. When a variable is of type unknown, you cannot directly perform operations on it unless you first check its type or assert it.

  • Use Cases: unknown is useful when you want to restrict operations on values of a certain type until you perform a type check. It’s more restrictive than any and forces you to explicitly handle or check the type before performing operations.
  • Behavior: Unlike any, you cannot directly access properties or call methods on a variable of type unknown without first narrowing the type.

Example:

let value: unknown = "Hello";
// Cannot do this directly
// value.toUpperCase();  // Error: Object is of type 'unknown'

// You must narrow the type first
if (typeof value === "string") {
  console.log(value.toUpperCase()); // Now it's safe
}

In summary, unknown provides more safety than any by requiring you to perform type checking or type assertions before using a value.


3. never Type

The never type represents values that never occur. It is used for functions that never return (such as those that throw an error or enter an infinite loop) or for values that should never be possible.

  • Use Cases:
    • Functions that throw errors or end execution without returning a value (e.g., throw or while(true)).
    • The never type is also used to signal that a function should never complete normally.
  • Behavior: A function that returns never is expected to either throw an error or go into an infinite loop and thus never return a value.

Example 1:

function throwError(message: string): never {
  throw new Error(message); // The function never returns, it always throws an error
}

function infiniteLoop(): never {
  while (true) {}
}

Example 2 (with exhaustive checks):

type Animal = "cat" | "dog";

function getAnimalSound(animal: Animal): string {
  switch (animal) {
    case "cat":
      return "Meow";
    case "dog":
      return "Woof";
    default:
      // TypeScript expects the 'default' to never be reached because we've covered all possible cases
      const _exhaustiveCheck: never = animal; // Ensures that `animal` is not mistakenly left unhandled
      throw new Error(`Unknown animal: ${_exhaustiveCheck}`);
  }
}

4. void Type

The void type represents the absence of any value. It is commonly used as the return type for functions that do not return a value, such as procedures or functions that only perform an operation.

  • Use Cases: The void type is typically used for functions that don’t return anything, like console.log() or event handler functions.
  • Behavior: A function with a return type of void is not expected to return a value, though it can still perform other operations.

Example:

function logMessage(message: string): void {
  console.log(message); // No return value, so the return type is void
}

const result = logMessage("Hello");
// result has type 'void', so it cannot be used in further operations

In summary:

  • void is used for functions that don’t return anything.
  • never is used for functions that never return (either because they throw an error or loop indefinitely).
  • any allows for any type of value, but it bypasses TypeScript’s type checking.
  • unknown also allows for any type of value, but it requires more explicit type checks before performing operations.

Summary Table:

TypeDescriptionUse Case Example
anyRepresents any type, allowing all operations and bypassing type checks.Migrating from JavaScript, dealing with dynamic content.
unknownSimilar to any, but requires explicit type checks or assertions before using the value.When you want to ensure type safety by validating types before performing operations.
neverRepresents values that never occur, like functions that always throw errors or run infinitely.Functions that always throw exceptions or enter infinite loops.
voidRepresents the absence of a value, used for functions that don’t return anything.Functions like console.log(), event handlers, or any function that doesn’t return a value.

These types help TypeScript developers write safer, more predictable code, enabling more rigorous type checking and clearer intent in the codebase.

Question: What are TypeScript interfaces, and how do they differ from types?

Answer:

In TypeScript, both interfaces and types are used to define the shape or structure of objects, but they differ in certain features and how they are used. Let’s dive into both concepts and explore the key differences.


1. Interfaces in TypeScript

An interface is a way to define a contract for objects, functions, or classes. It specifies the structure that an object or class should adhere to by defining the properties and methods that are expected.

  • Use Cases: Interfaces are primarily used to define the shape of objects, ensuring that they conform to a specific structure. They are commonly used for object-oriented programming and in defining API contracts.

  • Key Features:

    • Interfaces can define properties and methods for objects or classes.
    • Interfaces can be extended or implemented, making them highly flexible.
    • You can merge interfaces (declaration merging), where multiple declarations with the same name are automatically merged together.
    • Typically used for defining object shapes, function signatures, and class structures.

Example of an Interface:

interface Person {
  name: string;
  age: number;
  greet(): void;
}

const person: Person = {
  name: "John",
  age: 30,
  greet() {
    console.log("Hello, " + this.name);
  },
};

Interface Inheritance (Extending Interfaces):

interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

const dog: Dog = {
  name: "Rex",
  breed: "Golden Retriever",
};

2. Types in TypeScript

A type alias in TypeScript is used to define a name for any valid TypeScript type (not just objects, but also primitive types, unions, intersections, etc.). It’s a more general tool for creating type definitions.

  • Use Cases: Types are used to define complex structures or unions of types. They allow for greater flexibility when dealing with a wide variety of data types, including unions, intersections, and more complex constructs.

  • Key Features:

    • Types can represent primitive types, union types, intersection types, and more complex structures.
    • Types cannot be merged like interfaces.
    • Types are more flexible than interfaces because they can represent a broader range of type definitions.

Example of a Type Alias:

type Point = { x: number; y: number };

const point: Point = { x: 5, y: 10 };

Union and Intersection Types with type:

type Animal = { name: string };
type Dog = Animal & { breed: string }; // Intersection

const dog: Dog = { name: "Rex", breed: "Golden Retriever" };

type ID = string | number; // Union type

let userId: ID = 123; // Can be either a string or a number

Key Differences Between Interfaces and Types

FeatureInterfaceType
DefinitionUsed to define the shape of objects or function signatures.Used to define any type, including primitives, objects, and unions.
Extending/MergingCan be extended using extends and supports declaration merging (multiple interface declarations with the same name will merge).Cannot be merged. If you redefine the same type alias, it will cause an error.
Use with ObjectsPrimarily used to define object shapes, and class structures.Can also define object shapes, but more commonly used for unions, intersections, and other advanced types.
FlexibilityMore rigid, primarily used for defining object structures or contracts.More flexible, can represent a broader range of type constructs (e.g., unions, intersections).
Supports extendsYes, can extend other interfaces to create a new interface.No, but you can use intersections to combine multiple types.
Use with PrimitivesNot typically used with primitive types.Can be used to define unions of primitive types.
ImplementingCan be implemented by classes (class MyClass implements MyInterface).Cannot be implemented by classes.
Declaration MergingSupports declaration merging where multiple interfaces with the same name are automatically merged.Does not support declaration merging. Redefining a type will cause an error.

When to Use Interfaces vs Types

  • Use interfaces when:

    • You need to define object shapes, especially for objects and classes.
    • You want to take advantage of declaration merging or interface inheritance.
    • You are working in an object-oriented context and need to define contracts for classes to implement.
  • Use types when:

    • You need more flexibility and want to define complex types such as unions, intersections, or mapped types.
    • You need to define a type that can be a primitive, a function, or a combination of types.
    • You are working with complex data structures or need to define type aliases for things like function signatures or conditional types.

Summary Example

Using an Interface:

interface Car {
  brand: string;
  model: string;
  startEngine(): void;
}

class ElectricCar implements Car {
  brand: string;
  model: string;

  constructor(brand: string, model: string) {
    this.brand = brand;
    this.model = model;
  }

  startEngine() {
    console.log("Electric engine starting...");
  }
}

Using a Type:

type Car = {
  brand: string;
  model: string;
  startEngine(): void;
};

const car: Car = {
  brand: "Tesla",
  model: "Model S",
  startEngine() {
    console.log("Electric engine starting...");
  },
};

In the end, interfaces and types have a lot of overlap, and in many cases, you can use them interchangeably. However, the primary distinction is that interfaces are more geared toward object-oriented programming and class-based contracts, while types provide more flexibility for advanced type constructs.

Question: How would you define optional properties in a TypeScript interface or type?

Answer:

In TypeScript, both interfaces and types allow you to define optional properties for objects. This means that the property may or may not be present in the object. Optional properties are defined by appending a ? to the property name.


1. Optional Properties in an Interface

In an interface, optional properties are defined by adding a ? after the property name. This means the property is not required when creating an object that implements the interface.

Example:

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

const person1: Person = {
  name: "Alice",
};

const person2: Person = {
  name: "Bob",
  age: 30,
};

In this example:

  • The name property is required.
  • The age property is optional, so person1 does not need to include it, but person2 can include it.

2. Optional Properties in a Type

Similarly, in a type alias, optional properties are defined using the same ? syntax.

Example:

type Person = {
  name: string;  // Required property
  age?: number;  // Optional property
};

const person1: Person = {
  name: "Alice",
};

const person2: Person = {
  name: "Bob",
  age: 30,
};

Again, the name property is required, and the age property is optional.


Key Points:

  • Optional properties are defined with the ? symbol.
  • You can use optional properties in both interfaces and types.
  • Optional properties do not need to be provided when an object is created, but if they are provided, they must adhere to the specified type.

Example with Both Required and Optional Properties

You can combine required and optional properties in both interfaces and types.

Interface Example:

interface Product {
  id: number;        // Required property
  name: string;      // Required property
  description?: string; // Optional property
  price?: number;    // Optional property
}

const product1: Product = {
  id: 1,
  name: "Laptop",
};

const product2: Product = {
  id: 2,
  name: "Smartphone",
  description: "A high-end smartphone",
  price: 999,
};

Type Example:

type Product = {
  id: number;        // Required property
  name: string;      // Required property
  description?: string; // Optional property
  price?: number;    // Optional property
};

const product1: Product = {
  id: 1,
  name: "Laptop",
};

const product2: Product = {
  id: 2,
  name: "Smartphone",
  description: "A high-end smartphone",
  price: 999,
};

Summary:

  • To define an optional property in TypeScript, you use the ? after the property name.
  • This can be done both in interfaces and type aliases.
  • Optional properties allow you to create flexible structures where some properties may or may not be included in the object.

Question: What are the differences between interface and type in TypeScript?

Answer:

In TypeScript, both interface and type are used to define the shape or structure of data, but there are key differences in their features and use cases. Below is a comparison that highlights the differences:


1. Use Cases:

  • Interface: Primarily used to define the shape of objects and classes. It is a more object-oriented way to define a contract that can be implemented by classes or extended by other interfaces.

  • Type: More flexible and can define a wider variety of types, including objects, unions, intersections, tuples, primitives, and functions. It is generally more versatile than interfaces.


2. Extending and Implementing:

  • Interface:

    • Can be extended using the extends keyword to create more specific interfaces.
    • Can be implemented by classes.
    • Supports declaration merging, where multiple interface declarations with the same name will automatically merge their properties.

    Example of Extending an Interface:

    interface Animal {
      name: string;
    }
    
    interface Dog extends Animal {
      breed: string;
    }
    
    const dog: Dog = {
      name: "Rex",
      breed: "Golden Retriever",
    };

    Example of Implementing an Interface in a Class:

    interface Animal {
      name: string;
      makeSound(): void;
    }
    
    class Dog implements Animal {
      name: string;
    
      constructor(name: string) {
        this.name = name;
      }
    
      makeSound() {
        console.log("Woof!");
      }
    }
  • Type:

    • Cannot be implemented or extended by classes.
    • However, types can extend other types using intersections (&), allowing for flexibility and composition.

    Example of Extending a Type with Intersection:

    type Animal = {
      name: string;
    };
    
    type Dog = Animal & {
      breed: string;
    };
    
    const dog: Dog = {
      name: "Rex",
      breed: "Golden Retriever",
    };

3. Declaration Merging:

  • Interface: Supports declaration merging. This means if you declare the same interface multiple times in different parts of your code, TypeScript will merge them into one interface.

    Example of Declaration Merging:

    interface Person {
      name: string;
    }
    
    // Later in the code
    interface Person {
      age: number;
    }
    
    const person: Person = {
      name: "Alice",
      age: 30,
    };

    Here, TypeScript merges the two Person interfaces into a single interface with both name and age properties.

  • Type: Cannot be merged. If you declare the same type twice, it will result in a compiler error.

    Example (Compiler Error):

    type Person = {
      name: string;
    };
    
    // Later in the code (This will cause an error)
    type Person = {
      age: number;
    };

4. Complex Types (Unions, Intersections, Tuples, etc.):

  • Interface: Primarily designed for object shapes, but can also extend to functions and other object-oriented constructs. However, it is not as flexible for more complex types like unions or intersections.

  • Type: More flexible and can represent unions, intersections, tuples, and primitives, in addition to objects.

    Example of Union and Intersection Types with type:

    type Animal = { name: string };
    type Dog = Animal & { breed: string };  // Intersection type
    type Cat = Animal & { color: string };
    
    const dog: Dog = { name: "Rex", breed: "Golden Retriever" };
    const cat: Cat = { name: "Mittens", color: "Black" };
    
    type ID = string | number;  // Union type
    let userId: ID = 123;       // Can be either a string or a number

5. Support for typeof and keyof:

  • Interface: Interfaces do not directly support typeof and keyof constructs for primitives or values. They are designed for defining structures like objects, but not for more complex or dynamic types.

  • Type: type aliases are much more flexible in using typeof and keyof with both objects and primitive types. You can use these constructs to create types based on existing objects or variables.

    Example:

    type Person = { name: string; age: number };
    
    // Use `keyof` to extract keys from a type
    type PersonKeys = keyof Person;  // "name" | "age"
    
    const key: PersonKeys = "name"; // Correct
    
    // Use `typeof` to create a type based on a variable
    const dog = { name: "Rex", breed: "Golden Retriever" };
    type DogType = typeof dog;  // { name: string, breed: string }
    
    const anotherDog: DogType = { name: "Bella", breed: "Labrador" };

6. Use with Functions:

  • Interface: Interfaces can be used to describe function signatures, allowing you to specify the types of parameters and return values.

    Example with Functions:

    interface Greet {
      (name: string): string;
    }
    
    const greet: Greet = (name) => `Hello, ${name}`;
  • Type: type can also be used to define function signatures and is typically more flexible with unions or more complex function signatures.

    Example with Functions:

    type Greet = (name: string) => string;
    
    const greet: Greet = (name) => `Hello, ${name}`;

7. Compatibility with Other Types (Primitive, Class, etc.):

  • Interface: More suited for object-oriented programming and works well for defining classes or object shapes that adhere to specific contracts.

  • Type: More general-purpose and can be used for a wider variety of type constructs, including primitive types, tuples, unions, intersections, and mapped types.


Summary Table of Differences:

FeatureInterfaceType
Use CasePrimarily for defining object shapes and class contractsMore general-purpose, used for a variety of types (objects, unions, intersections, etc.)
ExtendingCan be extended using extends.Can extend using intersections (&), but not with extends.
MergingSupports declaration merging. Multiple interfaces with the same name are merged.Cannot be merged. Redeclaring a type causes an error.
Function SignaturesCan define function signatures.Can also define function signatures.
Complex TypesLess flexible with unions, intersections, or tuples.Highly flexible, supports unions, intersections, mapped types, etc.
ClassesCan be implemented by classes.Cannot be implemented by classes.
Object-OrientedWell-suited for object-oriented programming (interfaces can be implemented by classes).More flexible for complex types, but not designed for OOP.
Primitive TypesCannot directly represent primitive types or unions.Can represent primitive types, unions, and intersections.

Conclusion:

  • Use interface when you need to define contracts for objects or classes, especially in an object-oriented context.
  • Use type when you need more flexibility for defining unions, intersections, primitive types, and complex type constructs.

While there is a lot of overlap between interfaces and types in TypeScript, understanding when to use each can help improve the maintainability and clarity of your code.

Question: How do you define default parameters in TypeScript?

Answer:

In TypeScript, default parameters allow you to specify a default value for a function parameter if no argument is passed for that parameter when the function is called. This is similar to JavaScript’s default parameter functionality, but TypeScript adds type safety to ensure that the default values are compatible with the specified types.


Syntax for Default Parameters:

function functionName(param1: type = defaultValue): returnType {
  // function body
}
  • param1: The name of the parameter.
  • type: The type of the parameter (optional in some cases when TypeScript can infer the type).
  • defaultValue: The value assigned to param1 if no argument is provided.
  • returnType: The type of the value the function returns.

Example 1: Default Parameter with a Number

function greet(name: string = "Guest"): string {
  return `Hello, ${name}!`;
}

console.log(greet("Alice")); // Output: Hello, Alice!
console.log(greet());        // Output: Hello, Guest!

In this example:

  • The parameter name has a default value of "Guest".
  • If no argument is passed when calling greet(), "Guest" is used as the default value.

Example 2: Default Parameter with a Boolean

function isAdult(age: number, isAdult: boolean = false): boolean {
  return isAdult;
}

console.log(isAdult(25, true)); // Output: true
console.log(isAdult(17));       // Output: false

Here:

  • The isAdult parameter has a default value of false.
  • If the second argument is not provided, false is used.

Example 3: Default Parameter with an Object

function createPerson(name: string, age: number = 30): { name: string, age: number } {
  return { name, age };
}

console.log(createPerson("Alice")); // Output: { name: 'Alice', age: 30 }
console.log(createPerson("Bob", 25)); // Output: { name: 'Bob', age: 25 }

In this example:

  • The age parameter has a default value of 30.
  • If age is not provided, the function assigns 30 to age.

Example 4: Default Parameter in a Function with Multiple Parameters

function calculatePrice(price: number, tax: number = 0.1, discount: number = 0): number {
  return price + (price * tax) - discount;
}

console.log(calculatePrice(100));             // Output: 110 (tax is applied, no discount)
console.log(calculatePrice(100, 0.2));        // Output: 120 (tax is 20%, no discount)
console.log(calculatePrice(100, 0.2, 10));    // Output: 110 (tax is 20%, discount of 10)

In this example:

  • tax has a default value of 0.1 (10%).
  • discount has a default value of 0.

Type Inference and Default Parameters:

If TypeScript can infer the type of the parameter, you don’t need to explicitly define it. However, if the type is explicitly provided, it will ensure type safety with the default value.

function sum(a: number, b: number = 10): number {
  return a + b;
}

console.log(sum(5)); // Output: 15 (b is defaulted to 10)

In this example:

  • TypeScript infers that b is of type number because its default value is 10.
  • When calling sum(5), the value of b is automatically set to 10.

Key Points:

  • Default parameters are specified by assigning a value to the parameter in the function signature.
  • TypeScript checks that the default value matches the expected type of the parameter.
  • Default parameters can be useful for simplifying function calls by providing fallback values when arguments are not passed.

Summary:

You can define default parameters in TypeScript by assigning a default value directly in the function signature. This allows you to make some parameters optional while still ensuring type safety.

Question: Can you explain how TypeScript handles type inference?

Answer:

TypeScript’s type inference is a powerful feature that allows it to automatically determine the type of a variable, function return type, or parameter based on its value or usage. This reduces the need for explicit type annotations while still benefiting from TypeScript’s static type checking.

Here’s how TypeScript handles type inference in various scenarios:


1. Variable Type Inference

When you assign a value to a variable without explicitly declaring its type, TypeScript will automatically infer the type of that variable based on the assigned value.

Example 1: Simple Variable Inference

let num = 5; // TypeScript infers `num` as `number`
let name = "Alice"; // TypeScript infers `name` as `string`

In this example:

  • TypeScript infers that num is of type number because the value 5 is a number.
  • TypeScript infers that name is of type string because the value "Alice" is a string.

2. Function Return Type Inference

TypeScript can also infer the return type of a function based on the return statements in that function.

Example 2: Function Return Type Inference

function greet(name: string) {
  return `Hello, ${name}!`; // TypeScript infers return type as `string`
}

let greeting = greet("Alice");

In this case:

  • TypeScript infers that the return type of greet is string because the returned value is a string (the result of string interpolation).

However, if the function contains inconsistent return types, TypeScript will give an error because it cannot infer a single return type.

function inconsistentReturn(value: boolean) {
  if (value) {
    return "Yes";  // string
  } else {
    return 42;     // number
  }
}

Here, TypeScript will flag an error because it cannot infer a single type for the return value (string | number is not allowed unless explicitly defined).


3. Function Parameter Inference

When you define a function, TypeScript can infer the type of the parameters based on the provided values when the function is called, especially when you don’t provide explicit types.

Example 3: Parameter Type Inference

function multiply(x, y) {
  return x * y; // TypeScript infers both `x` and `y` as `number`
}

let result = multiply(5, 10); // `result` will be inferred as `number`

In this example:

  • Since x and y are used in a mathematical operation (x * y), TypeScript infers them as number types.

However, if you don’t use the parameters in a way that allows TypeScript to infer the type, or if you mix types (e.g., adding a string to a number), TypeScript may infer a broader type (like any or unknown).

function add(x, y) {
  return x + y; // Inferred as `any` due to mixed types
}

let sum = add(5, "hello"); // Inferred type of `sum` is `any`

4. Array Type Inference

When you initialize an array with values, TypeScript can infer the type of the array based on the types of the values inside it.

Example 4: Array Type Inference

let numbers = [1, 2, 3]; // TypeScript infers `numbers` as `number[]`
let strings = ["apple", "banana"]; // TypeScript infers `strings` as `string[]`
  • TypeScript infers that numbers is an array of number because the values inside are all numbers.
  • Similarly, strings is inferred as an array of string.

If you add a value of a different type, TypeScript will raise an error:

numbers.push("hello"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.

5. Object Type Inference

When you create an object, TypeScript infers the types of the object’s properties based on their values.

Example 5: Object Type Inference

let person = {
  name: "Alice", // inferred as `string`
  age: 30,       // inferred as `number`
}; 

// TypeScript infers `person` as `{ name: string; age: number; }`
  • TypeScript infers the type of person as an object with properties name: string and age: number.

If you try to assign a value that doesn’t match the inferred type, TypeScript will raise an error:

person.name = 123; // Error: Type 'number' is not assignable to type 'string'.

6. Type Inference with const vs let

  • const: TypeScript infers the most specific type when using const. It infers the literal value type.

    const x = 10; // Inferred type is `10`, not `number`.
    const str = "hello"; // Inferred type is `"hello"`, not `string`.
  • let: TypeScript infers a more general type when using let. The type is based on the value, but it’s more general.

    let y = 10; // Inferred type is `number`.
    let message = "hello"; // Inferred type is `string`.

Key Difference:

  • const creates a variable with a literal type, which means the value is tightly bound to its type.
  • let creates a variable with a general type, which can be reassigned to values of the same type.

7. Type Inference with Functions and Methods

TypeScript can infer the return type of a function or method based on the return statements.

Example 6: Function with Inferred Return Type

function sum(a: number, b: number) {
  return a + b;  // Inferred return type is `number`
}

let total = sum(10, 20); // `total` is inferred as `number`

If the return value is a more complex expression, TypeScript will infer the type based on that.

Example 7: Complex Return Type Inference

function getUserInfo() {
  return { name: "Alice", age: 30 };  // Inferred return type is `{ name: string; age: number; }`
}

let user = getUserInfo();

8. Generics and Type Inference

In TypeScript, generics are powerful tools for writing reusable code. TypeScript can infer the generic type from the passed argument.

Example 8: Generic Type Inference

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

let numberValue = identity(42);  // Inferred type is `number`
let stringValue = identity("hello");  // Inferred type is `string`

Here, TypeScript infers T based on the argument passed to identity.


Summary:

  • Type inference in TypeScript allows the compiler to automatically deduce the types of variables, function parameters, and return values based on their usage or assignment.
  • TypeScript infers types for variables, arrays, objects, and functions.
  • You can rely on TypeScript to reduce the need for explicit type annotations while still benefiting from type safety.
  • Type inference makes code less verbose while ensuring the correctness of data types.

Conclusion:

TypeScript’s type inference system enhances developer productivity by reducing the need for manual type annotations while maintaining type safety. It ensures that you can write clean, concise, and maintainable code, while still benefiting from the advantages of static type checking.

Question: What are TypeScript generics, and how do you use them?

Answer:

TypeScript generics are a way to write reusable, type-safe functions, classes, and interfaces. They allow you to define types that can work with any data type while still providing the benefits of type checking. Generics enable you to create flexible, yet strongly-typed components, functions, and classes that work with a wide range of types without losing type safety.

Generics are useful when you don’t know the exact type you will be working with ahead of time, but you still want to ensure that the type remains consistent throughout the function, class, or interface.


Basic Syntax of Generics

To define a generic type, you use angle brackets (< >) with a placeholder (usually a single letter like T, U, etc.) that represents a type. This placeholder will be replaced with the actual type when the function, class, or interface is used.

1. Generic Functions

In a function, generics allow you to pass a type parameter that will be used in the function’s return type or the types of its parameters.

Example 1: Generic Function

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

let num = identity(5);       // TypeScript infers T as `number`
let str = identity("hello"); // TypeScript infers T as `string`

Here:

  • T is a generic type that can represent any type, like number, string, or any other type.
  • TypeScript infers the type of T based on the argument passed to the function (identity(5) infers T as number, and identity("hello") infers T as string).

Generic Function with Explicit Type

You can also explicitly provide the type argument when calling the function.

let bool = identity<boolean>(true); // `T` is explicitly set to `boolean`

2. Generic Interfaces

Generics are commonly used in interfaces to define a structure that can work with multiple types while keeping the types consistent.

Example 2: Generic Interface

interface Box<T> {
  value: T;
}

let numberBox: Box<number> = { value: 10 };
let stringBox: Box<string> = { value: "hello" };

Here:

  • The Box interface is generic and can work with any type (represented by T).
  • The type of value inside the Box depends on the type passed to the Box interface (number, string, etc.).

3. Generic Classes

You can use generics with classes to define properties and methods that can handle different types of data.

Example 3: Generic Class

class Container<T> {
  private value: T;

  constructor(value: T) {
    this.value = value;
  }

  getValue(): T {
    return this.value;
  }
}

let numberContainer = new Container<number>(42);
let stringContainer = new Container<string>("hello");

console.log(numberContainer.getValue()); // Output: 42
console.log(stringContainer.getValue()); // Output: hello

Here:

  • The Container class is generic and accepts a type T.
  • The constructor and getValue method both work with the type T.

4. Generic Constraints

Sometimes, you may want to restrict the types that can be used with a generic. This can be done by adding constraints to the generic parameter. You can use a constraint to ensure that the generic type extends a certain type.

Example 4: Generic with Constraints

function loggingIdentity<T extends { length: number }>(arg: T): T {
  console.log(arg.length);
  return arg;
}

loggingIdentity("hello");  // Works, because string has a `length` property
loggingIdentity([1, 2, 3]); // Works, because arrays have a `length` property
loggingIdentity(5); // Error, because `number` does not have a `length` property

Here:

  • The generic T is constrained to types that have a length property (such as string or array).
  • TypeScript ensures that the function can only be called with types that meet the constraint.

5. Multiple Generics

You can use multiple type parameters in a single generic definition to create more complex types.

Example 5: Multiple Generics

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

let result = merge({ name: "Alice" }, { age: 30 });
// `result` is of type { name: string; age: number }

Here:

  • The function merge accepts two arguments: obj1 of type T and obj2 of type U.
  • The return type is a combination (T & U) of both types, which means the resulting object will have all properties from both obj1 and obj2.

6. Generic Functions in Arrays

You can define functions that accept or return arrays of generics.

Example 6: Generics in Arrays

function logArray<T>(arr: T[]): void {
  arr.forEach(item => console.log(item));
}

logArray([1, 2, 3]); // T is inferred as `number`
logArray(["apple", "banana"]); // T is inferred as `string`

Here:

  • T[] represents an array of elements of type T, and the function works with any type of array.
  • TypeScript automatically infers the type of the array elements (number[] or string[]).

7. Default Generic Types

You can specify a default type for a generic parameter, so if the caller doesn’t provide a type argument, TypeScript will use the default.

Example 7: Default Generic Type

function wrapInArray<T = string>(value: T): T[] {
  return [value];
}

let numberArray = wrapInArray(42); // Inferred as `number[]`
let stringArray = wrapInArray("hello"); // Inferred as `string[]`
let defaultArray = wrapInArray(); // Uses default type `string[]`

Here:

  • The default type for T is string, so if no type is provided, it defaults to string[].

8. Using Generics with keyof and infer

You can use keyof and infer with generics to extract types from objects and create more advanced patterns.

Example 8: keyof with Generics

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

const person = { name: "Alice", age: 30 };
let name = getProperty(person, "name"); // Type inferred as `string`

Here:

  • T is the type of obj, and K is a key from T.
  • The function returns the type of the property specified by key.

Benefits of Using Generics in TypeScript

  1. Code Reusability: Write functions, classes, and interfaces that work with any data type.
  2. Type Safety: Despite being flexible, generics maintain strict type checking.
  3. Increased Readability: Generics improve readability and reduce the need for type casting.
  4. Maintainability: Generic code is easier to maintain because it avoids duplicating similar logic for different types.

Summary

  • Generics allow you to create flexible, reusable, and type-safe code.
  • Use generics in functions, classes, and interfaces to work with multiple types while maintaining type safety.
  • Constraints can limit the types that can be used with a generic, and default types can be specified for generics.
  • TypeScript’s generics offer powerful features for writing scalable, type-safe code.

Generics enable you to write more abstract code that can be adapted to various types while still benefiting from TypeScript’s type checking and static analysis.

Question: How would you create a union type in TypeScript?

Answer:

In TypeScript, a union type allows a variable to be of one type or another. You can create a union type using the pipe (|) operator. This is useful when you want to allow a variable to hold values of multiple, but distinct, types.

1. Basic Union Type

You can define a variable that accepts multiple types by combining them with the pipe (|) operator.

Example 1: Basic Union Type

let value: string | number;

value = "hello";  // OK
value = 42;       // OK
value = true;     // Error: Type 'boolean' is not assignable to type 'string | number'

Here:

  • The variable value is a union type (string | number), meaning it can be either a string or a number.
  • TypeScript ensures that only values of type string or number are assigned to value.

2. Union Types with Arrays

You can create a union type within an array as well, allowing an array to hold multiple types.

Example 2: Union Type in Arrays

let mixedArray: (string | number)[] = ["hello", 42, "world"];
mixedArray.push(100);    // OK
mixedArray.push("test"); // OK
mixedArray.push(true);   // Error: Argument of type 'boolean' is not assignable to parameter of type 'string | number'.

Here:

  • mixedArray is an array that can hold either string or number.
  • TypeScript ensures that only string or number elements can be pushed into the array.

3. Union Types with Functions

You can use union types in function parameters to allow multiple types for an argument.

Example 3: Union Type in Function Parameters

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

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

Here:

  • The function printId accepts an argument id that can be either a string or a number.
  • TypeScript ensures that the argument passed is of type string or number.

4. Union Types with Objects

You can also use union types with objects. This is helpful when you want to define an object that can be of multiple shapes or types.

Example 4: Union Type in Objects

interface Car {
  make: string;
  model: string;
}

interface Bike {
  brand: string;
  type: string;
}

type Vehicle = Car | Bike;

function printVehicle(vehicle: Vehicle) {
  if ("make" in vehicle) {
    console.log(`Car: ${vehicle.make} ${vehicle.model}`);
  } else {
    console.log(`Bike: ${vehicle.brand} ${vehicle.type}`);
  }
}

const myCar: Car = { make: "Toyota", model: "Corolla" };
const myBike: Bike = { brand: "Trek", type: "Mountain" };

printVehicle(myCar); // Car: Toyota Corolla
printVehicle(myBike); // Bike: Trek Mountain

Here:

  • The Vehicle type is a union type of Car or Bike.
  • The printVehicle function checks which type of object it has received and prints the corresponding output.

5. Union Types with Literal Types

You can also create union types with literal types. This is useful when you want a variable to be limited to a set of specific values.

Example 5: Union with Literal Types

let status: "success" | "error" | "loading";

status = "success";  // OK
status = "loading";  // OK
status = "failed";   // Error: Type '"failed"' is not assignable to type '"success" | "error" | "loading"'

Here:

  • The variable status is constrained to the literal types "success", "error", or "loading".
  • TypeScript enforces that only one of these values can be assigned to status.

6. Using Union Types with Type Guards

Type guards help you narrow down the type of a variable when it has a union type. You can use the typeof or instanceof operators to check the type and narrow down the possible types within the union.

Example 6: Type Guards with Union Types

function printLength(value: string | string[]) {
  if (typeof value === "string") {
    console.log(`String length: ${value.length}`);
  } else {
    console.log(`Array length: ${value.length}`);
  }
}

printLength("Hello");        // String length: 5
printLength(["Apple", "Banana", "Cherry"]);  // Array length: 3

Here:

  • value can be either a string or an array of strings (string[]).
  • The typeof operator is used to narrow the type of value to either string or string[].

7. Combining Union and Intersection Types

You can combine union types with intersection types to create more complex types. This is useful when you want to have a type that can be one of several types but also include additional properties from another type.

Example 7: Union and Intersection Types

interface Car {
  make: string;
  model: string;
}

interface Electric {
  battery: number;
}

type ElectricCar = Car & Electric; // Intersection type

let myElectricCar: ElectricCar = {
  make: "Tesla",
  model: "Model 3",
  battery: 100,
};

type Vehicle = Car | ElectricCar;  // Union of Car and ElectricCar

Here:

  • ElectricCar is an intersection of Car and Electric, meaning it includes all properties from both interfaces.
  • Vehicle is a union of Car and ElectricCar, meaning it can be either a Car or an ElectricCar.

Summary

  • Union types allow a variable to be one of several types, defined using the pipe (|) operator.
  • You can use union types in variables, arrays, function parameters, objects, and literal types.
  • TypeScript helps you maintain type safety even when using union types, and you can leverage type guards to narrow down the type of a variable when necessary.
  • Union types are powerful for handling cases where a variable may have one of several types but still want to ensure type consistency.

Conclusion

Union types in TypeScript provide flexibility while maintaining strong typing. They allow variables, parameters, or return values to accept multiple types, making your code more expressive and adaptable. However, TypeScript ensures that you handle all possible types correctly, avoiding runtime errors.

Question: How do you define a tuple in TypeScript?

Answer:

In TypeScript, a tuple is a special type of array where you can specify the types of the elements at specific positions. Unlike regular arrays where all elements are of the same type, tuples allow elements of different types at different positions, and TypeScript ensures that the correct type is used at each position.

1. Basic Tuple Syntax

To define a tuple in TypeScript, you use square brackets [] to specify the types of the elements. Each type inside the square brackets represents the type of the respective element in the tuple.

Example 1: Defining a Tuple

let person: [string, number] = ["Alice", 30];

Here:

  • person is a tuple where the first element is a string (the name) and the second element is a number (the age).
  • TypeScript ensures that the first element is always a string and the second element is always a number.

2. Accessing Tuple Elements

You can access tuple elements by their index, just like arrays. However, TypeScript ensures type safety by checking the types of elements based on the tuple definition.

Example 2: Accessing Tuple Elements

let person: [string, number] = ["Alice", 30];

let name = person[0];  // type inferred as `string`
let age = person[1];   // type inferred as `number`

// person[0] = 123;  // Error: Type 'number' is not assignable to type 'string'

Here:

  • TypeScript infers that person[0] is of type string and person[1] is of type number.
  • If you try to assign a value of the wrong type (e.g., person[0] = 123), TypeScript will give an error.

3. Tuples with Optional Elements

Tuples can have optional elements, which means certain elements can be omitted. You define optional elements using the ? operator.

Example 3: Tuple with Optional Elements

let person: [string, number?, boolean?] = ["Alice", 30];
let anotherPerson: [string, number?, boolean?] = ["Bob", 25, true];

Here:

  • The tuple person can optionally have a number and boolean value, making the second and third elements optional.
  • anotherPerson includes all three elements (string, number, boolean).

4. Tuples with Rest Elements

You can use the rest element (...) to allow a variable number of elements at the end of a tuple. This is useful when you want to have a fixed number of elements at the start, but the rest of the tuple can be of a particular type and flexible in length.

Example 4: Tuple with Rest Elements

let data: [string, ...number[]] = ["Alice", 30, 40, 50];
let anotherData: [string, ...boolean[]] = ["Active", true, false];

Here:

  • The tuple data starts with a string followed by any number of number elements.
  • The tuple anotherData starts with a string followed by any number of boolean elements.

5. Named Tuples (Introduced in TypeScript 4.0)

Starting with TypeScript 4.0, you can give names to the tuple elements, which makes the code more readable.

Example 5: Named Tuples

let person: [name: string, age: number] = ["Alice", 30];

Here:

  • The tuple person has named elements: name (a string) and age (a number).
  • This improves the readability of the code when accessing tuple elements.

6. Using Tuples in Functions

You can also use tuples in function parameters and return types to represent fixed-sized and typed collections of values.

Example 6: Tuples in Function Parameters

function printPersonInfo(person: [string, number]) {
  console.log(`Name: ${person[0]}, Age: ${person[1]}`);
}

let person: [string, number] = ["Alice", 30];
printPersonInfo(person);  // Output: Name: Alice, Age: 30

Here:

  • The function printPersonInfo accepts a tuple person with a string and number as parameters.
  • It logs the values of the tuple to the console.

Example 7: Tuples as Function Return Types

function getCoordinates(): [number, number] {
  return [40.7128, -74.0060]; // returns a tuple of latitude and longitude
}

let coords = getCoordinates();
console.log(coords); // Output: [40.7128, -74.0060]

Here:

  • The function getCoordinates returns a tuple of type [number, number] representing latitude and longitude.

7. Type Inference with Tuples

TypeScript can often infer the type of a tuple if you provide the initial values, but you can also explicitly specify the type if needed.

Example 8: Type Inference in Tuples

let person = ["Alice", 30]; // TypeScript infers: [string, number]

let coordinates: [number, number] = [40.7128, -74.0060]; // Explicitly typed

Here:

  • TypeScript infers the type of person as [string, number].
  • You can also explicitly define the tuple’s type as [number, number] for coordinates.

8. Tuples vs Arrays

The main difference between tuples and arrays in TypeScript is that:

  • Tuples have a fixed size and specific types for each element at each index.
  • Arrays are more flexible and can hold a variable number of elements, all of which are of the same type.

For example:

let arr: number[] = [1, 2, 3]; // Array of numbers, any length
let tuple: [string, number] = ["Alice", 30]; // Tuple with exact types and fixed length

Summary

  • Tuples in TypeScript are arrays with fixed types at specific positions.
  • You define a tuple with square brackets ([]), specifying types for each position.
  • Tuples can have optional or rest elements.
  • Tuples can also have named elements for better readability.
  • They are particularly useful when you need to represent fixed-size collections with different types of elements.

Conclusion

Tuples are an essential feature in TypeScript when you need a fixed-size collection of elements with different types. They allow you to express complex data structures while keeping type safety, making your code more predictable and less prone to errors.

Question: What are the different ways to declare variables in TypeScript, and what are the differences?

Answer:

In TypeScript, there are three main ways to declare variables: let, const, and var. Each has its own use case and behavior, which can affect the way variables are scoped, reassigned, and hoisted. Here’s a breakdown of each, including the differences:


1. let

  • Scope: Block-scoped (limited to the block, statement, or expression where it is defined).
  • Reassignment: Variables declared with let can be reassigned to a new value.
  • Hoisting: let variables are hoisted, but not initialized. This means you cannot access the variable before its declaration line in the code.

Example:

let x = 10;
x = 20;  // OK, because 'x' is mutable.

if (true) {
  let x = 30;  // This 'x' is scoped to the block, not the outer 'x'
  console.log(x);  // 30
}

console.log(x);  // 20 (the outer 'x' remains unaffected)
  • Usage: let is typically used when you expect the variable’s value to change or if you want block-level scoping (e.g., inside loops or conditionals).

2. const

  • Scope: Block-scoped (same as let).
  • Reassignment: Variables declared with const cannot be reassigned after their initial declaration.
  • Hoisting: const variables are hoisted, but they must be initialized at the time of declaration. You cannot reference them before the declaration line due to the temporal dead zone (TDZ).

Example:

const x = 10;
x = 20;  // Error: Cannot assign to 'x' because it is a constant

if (true) {
  const x = 30;  // This 'x' is scoped to the block, not the outer 'x'
  console.log(x);  // 30
}

console.log(x);  // 10 (the outer 'x' remains unaffected)
  • Usage: const is used when you want to ensure that a variable’s value does not change throughout its scope. It’s commonly used for values that are not meant to be reassigned (e.g., constants, configuration values).

Note: const only prevents reassignment of the variable itself. If the variable points to an object or array, the contents of the object/array can still be modified.

Example with Objects:

const person = { name: "Alice", age: 30 };
person.age = 31;  // OK, because we are modifying the contents of the object, not the reference

person = { name: "Bob", age: 25 };  // Error: Cannot assign to 'person' because it is a constant

3. var

  • Scope: Function-scoped, not block-scoped. A var declared inside a block (e.g., inside a loop or if statement) is accessible outside that block as well, within the enclosing function.
  • Reassignment: Variables declared with var can be reassigned.
  • Hoisting: var variables are hoisted to the top of their scope and initialized with undefined. You can reference the variable before the actual declaration line, but it will have the value undefined until it is assigned a value.

Example:

function example() {
  if (true) {
    var x = 10;
  }
  console.log(x);  // 10 (accessible outside the block due to function-scoping)
}

example();
  • Usage: var is generally not recommended in modern TypeScript/JavaScript because it does not support block scoping, which can lead to unexpected behavior, especially in loops and conditionals. It’s mostly used for backward compatibility with older code.

Differences between let, const, and var

Featureletconstvar
ScopeBlock-scopedBlock-scopedFunction-scoped
ReassignableYesNo (cannot be reassigned)Yes
HoistingHoisted but not initializedHoisted but not initializedHoisted and initialized with undefined
Temporal Dead Zone (TDZ)Yes, access before declaration results in an errorYes, access before declaration results in an errorNo, can access variables before declaration (but it will be undefined)
Block-level UsageYesYesNo (can lead to issues in block-level constructs like loops)

4. Additional Points:

  • let and const are preferred in modern TypeScript for their block-scoping behavior, which leads to fewer bugs and more predictable code.
  • var should generally be avoided in favor of let or const because of its function-scoping and the risk of unintentional variable shadowing.
  • Use const when possible to indicate that the value should not change. This helps with immutability and makes your code easier to reason about.

Summary

  • let: Use when you need a variable that can be reassigned and you need block-level scoping.
  • const: Use when you do not want a variable to be reassigned, providing block-level scoping. Great for constants and immutability.
  • var: Use sparingly (or avoid). It’s function-scoped, which can lead to unintended behaviors, especially inside loops or conditionals.

TypeScript (and modern JavaScript) encourages using let and const over var to ensure better scoping and reduce potential bugs.

Question: What is the never type in TypeScript, and when would you use it?

Answer:

In TypeScript, the never type represents a value that never occurs. It is a special type used in scenarios where a function or a variable cannot complete normally. It indicates that the function will either throw an error or enter an infinite loop, and thus, it will never return a value or terminate successfully.

The never type is useful for functions that do not return and for types of values that can never exist. It is the opposite of void, which is used for functions that return undefined or null.


1. never in Function Return Types

One of the most common use cases for never is in the return type of a function that never completes its execution successfully. These are typically functions that throw an exception or enter an infinite loop.

Example 1: Function That Throws an Error

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

throwError("Something went wrong!");  // This function will never return.
  • The throwError function has the return type never because it throws an error and does not return anything.
  • Once an exception is thrown, the function execution stops, and no value is returned, making it an appropriate use case for the never type.

Example 2: Function That Causes an Infinite Loop

function infiniteLoop(): never {
  while (true) {
    // Infinite loop, never returns a value.
  }
}

infiniteLoop();  // This will never complete or return.
  • In the infiniteLoop function, there is an infinite loop, which means the function will never reach a return statement, hence it has the never return type.

2. never in Exhaustive Checks

The never type is also useful in exhaustive checks when you are working with union types. It’s a way to ensure that all possible cases of a union type are covered. If you forget to handle a case, TypeScript will raise an error.

Example 3: Exhaustive Check with a switch Statement

type Shape = "circle" | "square" | "triangle";

function getArea(shape: Shape): number {
  switch (shape) {
    case "circle":
      return Math.PI * 10 * 10;  // Example for a circle
    case "square":
      return 10 * 10;  // Example for a square
    case "triangle":
      return 0.5 * 10 * 10;  // Example for a triangle
    default:
      // TypeScript will expect that this case never happens because of the exhaustive check.
      // If we add a new shape type later and forget to handle it, TypeScript will warn us.
      throw new Error(`Unknown shape: ${shape}`);
  }
}
  • In the default case, if a new shape type is added to the Shape type and is not handled in the switch statement, TypeScript will correctly infer that this is an unhandled case and raise an error.
  • This behavior ensures exhaustive checking, meaning that all possible cases are addressed.

3. never as a Return Type for never-Ending Operations

The never type can also be used in cases where a function is specifically intended to never return, such as when using certain operations that stop the execution flow, like terminating a process or system.

Example 4: Using never in Type Guards

function assertNever(value: never): never {
  throw new Error(`Unexpected value: ${value}`);
}

type Animal = { type: "dog"; bark: () => void } | { type: "cat"; meow: () => void };

function handleAnimal(animal: Animal) {
  switch (animal.type) {
    case "dog":
      animal.bark();
      break;
    case "cat":
      animal.meow();
      break;
    default:
      // The default case is called when `animal.type` is not a "dog" or "cat".
      // This ensures that all cases of the Animal type are exhausted.
      assertNever(animal);  // Will throw an error if we encounter an unexpected animal type.
  }
}
  • The assertNever function is a type guard used to enforce exhaustive checks on the animal type. If a new animal type is added but not handled in the switch statement, the TypeScript compiler will catch the error when assertNever is called with a value that is not of type never.

4. never as a Type for Variables That Shouldn’t Exist

You can use never when you define a variable that should never hold a valid value, essentially saying that this variable can never have any valid state. For example, if you’re dealing with a system state that should never happen.

Example 5: never in Type Declarations

type EmptyType = never;

let emptyValue: EmptyType;
// emptyValue = "something";  // Error: Type 'string' is not assignable to type 'never'.
  • The type never ensures that emptyValue can never be assigned any value, as never represents a state that should never occur.

Summary of Use Cases for never:

  1. Functions that never return (e.g., throwing errors, infinite loops).
  2. Exhaustive checks for union types, to ensure that all cases are handled.
  3. Type guards for enforcing exhaustive checks in switch/case statements.
  4. Variables that can never have a valid value or can represent an impossible state.

Conclusion:

The never type is used in TypeScript to represent situations where a value or return type is impossible or unreachable. It’s commonly used for functions that throw exceptions or run indefinitely, and it also plays a crucial role in exhaustive type checks to ensure that all possible cases are covered. Understanding and using the never type properly can help prevent logical errors and improve type safety in your TypeScript code.

Question: How does TypeScript support asynchronous programming (e.g., Promises and async/await)?

Answer:

TypeScript has strong support for asynchronous programming, providing type safety and better development experience with asynchronous operations such as Promises and the async/await syntax. This support enhances the ability to work with asynchronous code while also ensuring that you can catch potential errors at compile time rather than runtime.


1. Promises in TypeScript

A Promise is an object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value. TypeScript’s type system can infer and enforce the types of values that Promises resolve to, which adds type safety.

Example: Using Promises

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

fetchData().then(result => {
  console.log(result);  // "Data fetched successfully"
});
  • Promise Types: TypeScript allows you to define the type of data the Promise will resolve to (e.g., Promise<string>), which helps ensure that the value returned by the Promise matches the expected type.

    In the example above, fetchData is a function that returns a Promise<string>, meaning the promise resolves to a string value. TypeScript ensures that only values of the correct type (in this case, string) are resolved by the Promise.

Handling Promise Rejection:

function fetchDataWithError(): Promise<string> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject("Failed to fetch data");
    }, 1000);
  });
}

fetchDataWithError()
  .then(result => console.log(result))
  .catch(error => console.error(error));  // "Failed to fetch data"
  • TypeScript ensures that the .catch() handler receives the appropriate type of error (string in this case).

2. async/await Syntax in TypeScript

TypeScript fully supports the async and await syntax introduced in ES2017 (ES8), which makes working with asynchronous code more readable and easier to maintain.

2.1 async Functions

An async function is a function that always returns a Promise, and inside that function, you can use the await keyword to pause execution until the Promise is resolved. The function returns a Promise that resolves to whatever value you return from the function.

Example: async Function

async function fetchData(): Promise<string> {
  return "Data fetched successfully";
}

fetchData().then(result => {
  console.log(result);  // "Data fetched successfully"
});
  • Even though fetchData appears to return a string directly, it actually returns a Promise<string> due to the async modifier.

2.2 Using await

The await keyword is used inside async functions to pause the execution until the Promise resolves and then returns the resolved value. This makes the code look and behave more like synchronous code, but it’s still non-blocking.

Example: async/await Syntax

async function fetchData(): Promise<string> {
  const response = await new Promise<string>((resolve) => {
    setTimeout(() => resolve("Data fetched successfully"), 1000);
  });
  return response;
}

async function main() {
  const result = await fetchData();
  console.log(result);  // "Data fetched successfully"
}

main();
  • The await expression pauses the function execution until the Promise is resolved and returns the value from the resolved Promise. TypeScript ensures the type of the result is correct based on the Promise’s resolved value.

3. Type Inference with async/await

TypeScript can infer the return type of async functions automatically. For instance, if an async function returns a Promise<string>, TypeScript infers this and provides type safety. However, it’s still good practice to specify return types explicitly for clarity.

Example: Type Inference in async/await

async function fetchData(): Promise<string> {
  return "Hello, World!";
}

const result = fetchData();
  • TypeScript automatically infers that fetchData() returns a Promise<string>, and this provides type checking for subsequent .then() or await calls.

4. Error Handling with async/await

Error handling in asynchronous code is simpler with async/await because you can use traditional try/catch blocks, which makes your code easier to read compared to using .then() and .catch() with Promises.

Example: Error Handling in async/await

async function fetchDataWithError(): Promise<string> {
  throw new Error("Failed to fetch data");
}

async function main() {
  try {
    const result = await fetchDataWithError();
    console.log(result);
  } catch (error) {
    console.error(error.message);  // "Failed to fetch data"
  }
}

main();
  • The try/catch block provides a synchronous-like flow for handling errors that might arise in the asynchronous function.

5. Parallel Execution with async/await

While async/await is great for sequential asynchronous operations, you may need to execute multiple asynchronous tasks in parallel. In such cases, you can use Promise.all() to wait for multiple Promises to resolve concurrently.

Example: Running Promises in Parallel

async function fetchData1(): Promise<string> {
  return "Data 1";
}

async function fetchData2(): Promise<string> {
  return "Data 2";
}

async function main() {
  const [data1, data2] = await Promise.all([fetchData1(), fetchData2()]);
  console.log(data1);  // "Data 1"
  console.log(data2);  // "Data 2"
}

main();
  • Promise.all() allows you to run fetchData1() and fetchData2() in parallel, making your code more efficient when you have multiple independent asynchronous operations.

6. TypeScript and async/await Return Types

TypeScript’s type system makes it easy to define and check the return types of async functions. When you use async, TypeScript automatically wraps the return value in a Promise, which ensures that the caller can handle it as a Promise.

Example: Handling Return Types in async/await

async function fetchData(): Promise<number> {
  return 42;  // Implicitly returns a Promise<number>
}

async function main() {
  const result = await fetchData();
  console.log(result);  // 42
}
  • Even though fetchData() returns a number, the return type is automatically wrapped in a Promise<number>. TypeScript checks that the value returned from fetchData is a valid number, ensuring type safety.

Summary

TypeScript provides excellent support for asynchronous programming using Promises and the async/await syntax, offering both ease of use and robust type safety. The main benefits of TypeScript’s support for asynchronous programming are:

  1. Type Safety: TypeScript ensures the types of resolved values from Promises, providing compile-time validation.
  2. Readable Code: The async/await syntax helps make asynchronous code look synchronous and easier to follow.
  3. Error Handling: TypeScript allows for clean error handling with try/catch blocks in asynchronous functions.
  4. Parallel Execution: You can run multiple asynchronous operations in parallel using Promise.all(), improving performance.
  5. Automatic Type Inference: TypeScript infers the return type of async functions, reducing boilerplate code and providing strong typing.

With these features, TypeScript enhances the development experience when working with asynchronous code, making it both safer and more maintainable.

Question: What are type guards in TypeScript, and how do they work?

Answer:

Type guards in TypeScript are expressions or functions that allow you to narrow down the type of a variable within a specific scope. By using type guards, you can ensure that TypeScript understands the type of a variable more precisely within a block of code, improving type safety and enabling better type inference.

1. Basic Concept of Type Guards

A type guard is any expression or check that narrows down the type of a value from a broader type (e.g., any, unknown, object, string | number) to a more specific type (e.g., string, number). This allows you to safely perform operations that are valid only for that specific type, without causing type errors.

TypeScript uses these narrowing techniques to determine the specific type of a variable at runtime and tailor type checking accordingly.


2. Common Type Guards in TypeScript

2.1 typeof Type Guard

The typeof operator can be used to narrow the type of a variable when you check its primitive type (number, string, boolean, symbol, undefined, object, etc.).

Example: Using typeof

function printLength(input: string | number): void {
  if (typeof input === "string") {
    console.log(input.length);  // `input` is narrowed to `string` here.
  } else {
    console.log(input.toFixed(2));  // `input` is narrowed to `number` here.
  }
}

printLength("Hello");  // 5
printLength(123.456);  // 123.46
  • Explanation: In the if block, input is narrowed to type string because of the typeof input === "string" check, which ensures that input.length is a valid operation. Similarly, inside the else block, input is inferred to be a number.

2.2 instanceof Type Guard

The instanceof operator can be used to check if an object is an instance of a particular class or a subclass.

Example: Using instanceof

class Dog {
  bark() {
    console.log("Woof");
  }
}

class Cat {
  meow() {
    console.log("Meow");
  }
}

function makeSound(animal: Dog | Cat): void {
  if (animal instanceof Dog) {
    animal.bark();  // `animal` is narrowed to `Dog` here
  } else {
    animal.meow();  // `animal` is narrowed to `Cat` here
  }
}

makeSound(new Dog());  // Woof
makeSound(new Cat());  // Meow
  • Explanation: The instanceof check narrows the animal variable to either the Dog or Cat class, allowing you to call the respective methods (bark or meow) based on the class of the object.

2.3 Custom Type Guards

You can also create your own custom type guard functions that return a boolean value and narrow the type of a variable within a if or else block. Custom type guards are typically defined using user-defined type predicates.

Example: Custom Type Guard Function

interface Fish {
  swim(): void;
}

interface Bird {
  fly(): void;
}

function isFish(animal: Fish | Bird): animal is Fish {
  return (animal as Fish).swim !== undefined;
}

function makeMove(animal: Fish | Bird): void {
  if (isFish(animal)) {
    animal.swim();  // `animal` is narrowed to `Fish` here
  } else {
    animal.fly();   // `animal` is narrowed to `Bird` here
  }
}

const fish: Fish = { swim() { console.log("Swimming"); } };
const bird: Bird = { fly() { console.log("Flying"); } };

makeMove(fish);  // Swimming
makeMove(bird);  // Flying
  • Explanation: The isFish function is a custom type guard that checks whether the animal object has a swim method. This allows TypeScript to narrow the type of animal to Fish when isFish(animal) returns true, and to Bird otherwise.

In the makeMove function, TypeScript knows that inside the if block, animal is a Fish, and inside the else block, animal is a Bird.


3. Type Guard with in Operator

The in operator can be used to check if a property exists in an object, which can be helpful for narrowing down object types.

Example: Using in Operator

interface Circle {
  radius: number;
}

interface Square {
  sideLength: number;
}

function getArea(shape: Circle | Square): number {
  if ("radius" in shape) {
    return Math.PI * shape.radius ** 2;  // `shape` is narrowed to `Circle` here
  } else {
    return shape.sideLength ** 2;  // `shape` is narrowed to `Square` here
  }
}

const circle: Circle = { radius: 10 };
const square: Square = { sideLength: 5 };

console.log(getArea(circle));  // 314.159
console.log(getArea(square));  // 25
  • Explanation: The in operator is used to check if the radius property exists in shape. If it does, TypeScript narrows the type of shape to Circle; otherwise, it is narrowed to Square.

4. Type Guard with null and undefined

TypeScript also narrows types by checking whether a value is null or undefined. This is especially useful in cases where you are dealing with union types that include null or undefined.

Example: null and undefined Type Guard

function greet(name: string | null): void {
  if (name !== null) {
    console.log(`Hello, ${name}`);
  } else {
    console.log("Hello, Guest");
  }
}

greet("Alice");  // Hello, Alice
greet(null);      // Hello, Guest
  • Explanation: TypeScript narrows the name type to string in the if block (name !== null), and to null in the else block. This ensures that operations on name are safe.

5. Type Guard with unknown Type

If you have a variable with the unknown type (often used when you don’t know the type of the value ahead of time), you must use a type guard before you can perform any operations on that variable.

Example: Using Type Guard with unknown

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

function printString(value: unknown): void {
  if (isString(value)) {
    console.log(value.toUpperCase());  // Safe to use string methods
  } else {
    console.log("Not a string");
  }
}

printString("hello");  // HELLO
printString(123);      // Not a string
  • Explanation: The isString function is a custom type guard that narrows value to string when the type is indeed a string. This allows safe use of string methods like .toUpperCase().

Summary

Type guards in TypeScript are techniques that help narrow down the type of a variable within a certain scope, making it possible to perform type-safe operations on variables. Some common ways to implement type guards include:

  1. typeof operator — For checking the primitive types (e.g., string, number).
  2. instanceof operator — For checking if an object is an instance of a particular class.
  3. Custom type guard functions — Using user-defined type predicates to create your own type narrowing logic.
  4. in operator — To check if a property exists in an object, useful for discriminating between different object types.
  5. Handling null and undefined — TypeScript automatically narrows types when checking for null or undefined.

Using type guards enables TypeScript to perform static type checking more effectively, improving the safety and robustness of your code.

Question: How do you define and use type aliases in TypeScript?

Answer:

Type aliases in TypeScript allow you to create a new name for a type, which can be useful to simplify complex types, improve code readability, or avoid redundancy. A type alias does not create a new type; instead, it creates a new name for an existing type.

You can use a type alias for any type, including primitive types, union types, intersection types, tuples, and more.

1. Basic Syntax of Type Aliases

The basic syntax for defining a type alias is as follows:

type TypeAliasName = TypeExpression;
  • type: Keyword used to define a type alias.
  • TypeAliasName: The name of the new type alias.
  • TypeExpression: The type that you want to alias (e.g., string, number, or complex types like union or intersection types).

2. Example: Defining a Simple Type Alias

Defining a type alias for a string:

type MyString = string;

let name: MyString = "John";  // Valid
let age: MyString = "30";     // Valid
  • Explanation: The MyString alias now represents the string type. This is equivalent to using string directly, but it allows for better readability and reuse.

3. Example: Type Aliases for Objects

You can define type aliases for objects, which can be helpful for complex data structures.

type Person = {
  name: string;
  age: number;
};

const person1: Person = {
  name: "Alice",
  age: 25,
};
  • Explanation: The Person type alias is an object with name and age properties. This alias can be used to define multiple Person objects without repeating the same object structure.

4. Example: Type Aliases with Union Types

You can use type aliases with union types to define a variable that can have multiple possible types.

type StringOrNumber = string | number;

let value: StringOrNumber;

value = "Hello";   // Valid
value = 123;       // Valid
value = true;      // Error: Type 'boolean' is not assignable to type 'StringOrNumber'.
  • Explanation: The StringOrNumber alias represents a type that can be either a string or a number. This alias can simplify union types and make the code more readable.

5. Example: Type Aliases with Intersection Types

You can use intersection types to combine multiple types into one.

type Shape = {
  area: number;
};

type Color = {
  color: string;
};

type ColoredShape = Shape & Color;

const coloredShape: ColoredShape = {
  area: 100,
  color: "blue",
};
  • Explanation: The ColoredShape alias is an intersection of Shape and Color, meaning it must have all the properties of both Shape and Color. In this case, ColoredShape must include both area (number) and color (string).

6. Example: Type Aliases for Tuples

You can use type aliases to define tuple types.

type Point = [number, number];

const p1: Point = [1, 2];  // Valid
const p2: Point = [1, "2"];  // Error: Type 'string' is not assignable to type 'number'.
  • Explanation: The Point alias represents a tuple with exactly two numbers. It can be used for defining points in a 2D coordinate system.

7. Example: Type Aliases with Function Types

You can define function types using type aliases, which can help in situations where a function type is complex or needs to be reused.

type GreetFunction = (name: string) => string;

const greet: GreetFunction = (name) => {
  return `Hello, ${name}`;
};

console.log(greet("Alice"));  // Output: Hello, Alice
  • Explanation: The GreetFunction alias defines a function type that takes a string as an argument and returns a string. This makes it easier to declare function types and ensures consistency in the function signatures.

8. Type Aliases for Complex Types

You can use type aliases to define more complex types, such as generic types, objects with optional properties, or other advanced types.

type Response<T> = {
  status: number;
  data: T;
};

const successResponse: Response<string> = {
  status: 200,
  data: "Operation successful",
};

const errorResponse: Response<{ message: string }> = {
  status: 500,
  data: { message: "Internal server error" },
};
  • Explanation: The Response<T> alias defines a generic type that can be used with any data type (T). This allows you to define responses with different data types while keeping the same structure for the status.

9. Advantages of Using Type Aliases

  • Code Simplification: Type aliases allow you to define complex types more succinctly, making the code easier to read and maintain.
  • Reusability: Once defined, a type alias can be reused throughout the code, reducing redundancy and improving consistency.
  • Readability: Type aliases can make the code more expressive. For example, type Person = { name: string, age: number } is more readable than repeatedly defining the object type structure inline.
  • Type Safety: Type aliases provide additional type safety by ensuring that variables, function parameters, and return types match the expected structure.

10. Limitations of Type Aliases

  • No Extension: Unlike interfaces, type aliases cannot be extended or implemented. For more complex inheritance-based patterns, interfaces might be a better choice.
type Animal = { name: string };
type Dog = Animal & { breed: string };  // Intersection types work but cannot be extended like interfaces

// The following will result in an error:
type Animal = { name: string };
type Dog extends Animal { breed: string };  // Error: 'extends' can only be used with classes or interfaces

Conclusion

Type aliases in TypeScript provide a powerful way to create reusable and readable types for various data structures, including objects, unions, intersections, functions, and more. They can simplify your code and help maintain strong type safety. While similar to interfaces in some respects, type aliases offer additional flexibility for working with advanced types like union and intersection types.

Question: What is the role of strict mode in TypeScript?

Answer:

Strict mode in TypeScript is a set of compiler options that enforce stricter type-checking rules to help developers write more reliable and error-free code. When strict mode is enabled, TypeScript performs additional checks and generates more warnings or errors when potential issues are detected. This helps catch mistakes early in development.

Enabling strict mode ensures better type safety and improves code quality by:

  1. No Implicit any: Variables or function parameters must have explicit types. This prevents TypeScript from automatically inferring the any type when types are not specified, reducing the risk of unexpected runtime errors.

  2. Strict Null Checks: TypeScript will treat null and undefined as distinct types, so you cannot assign null or undefined to variables unless explicitly allowed. This reduces issues related to null reference errors.

  3. Strict Function Types: Functions with mismatched parameter or return types will raise errors. It ensures that functions are used correctly according to their type signatures.

  4. Strict Property Initialization: It ensures that class properties are initialized properly either in the constructor or with a default value, preventing uninitialized properties.

  5. No Implicit This: The value of this inside functions is strictly typed, avoiding issues where this is implicitly any in functions or methods.

Strict mode can be enabled by setting "strict": true in the tsconfig.json file or by enabling specific strict flags such as "noImplicitAny", "strictNullChecks", etc.

Using strict mode can significantly improve the reliability and maintainability of TypeScript code by enforcing better type safety and catching potential errors during development.

Question: What are TypeScript decorators and how are they used?

Answer:

TypeScript decorators are a special kind of declaration that can be attached to classes, methods, properties, or parameters, allowing you to modify their behavior at runtime. They are a powerful feature in TypeScript, heavily inspired by the decorator pattern in object-oriented design, and are widely used in frameworks like Angular and NestJS for dependency injection, logging, and more.

Decorators are essentially functions that can be applied to different parts of a TypeScript program to add metadata, alter functionality, or provide additional behavior without changing the original code structure.

Types of Decorators in TypeScript:

  1. Class Decorators: Applied to the class constructor. These decorators are used to modify or enhance the behavior of the class itself.

    function MyClassDecorator(constructor: Function) {
      console.log('Class decorated:', constructor);
    }
    
    @MyClassDecorator
    class MyClass {
      constructor(public name: string) {}
    }

    Here, the MyClassDecorator is applied to MyClass, and you can add additional logic to the class constructor.

  2. Method Decorators: Applied to the methods of a class. These decorators can modify the behavior of method execution.

    function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
      const originalMethod = descriptor.value;
      descriptor.value = function(...args: any[]) {
        console.log(`Called ${propertyKey} with args: ${JSON.stringify(args)}`);
        return originalMethod.apply(this, args);
      };
    }
    
    class MyClass {
      @Log
      myMethod(x: number, y: number) {
        return x + y;
      }
    }
    
    const obj = new MyClass();
    obj.myMethod(5, 10);  // Logs: Called myMethod with args: [5,10]
  3. Property Decorators: Applied to class properties. These decorators are used to attach metadata or modify property behavior.

    function ReadOnly(target: any, propertyKey: string) {
      const descriptor = Object.getOwnPropertyDescriptor(target, propertyKey);
      if (descriptor) {
        descriptor.writable = false;
        Object.defineProperty(target, propertyKey, descriptor);
      }
    }
    
    class MyClass {
      @ReadOnly
      name: string = 'TypeScript';
    }

    The ReadOnly decorator makes the name property immutable.

  4. Parameter Decorators: Applied to method parameters, allowing you to modify or capture information about the parameters when the method is called.

    function LogParameter(target: any, methodName: string, parameterIndex: number) {
      console.log(`Parameter at index ${parameterIndex} in method ${methodName} is being decorated.`);
    }
    
    class MyClass {
      greet(@LogParameter name: string) {
        console.log(`Hello, ${name}`);
      }
    }

How Decorators Work:

  • Decorators are called at runtime when the class or method they are attached to is initialized.
  • The decorator function receives metadata about the target (e.g., class, method, property, or parameter).
  • You can modify this metadata, attach new properties, or alter the behavior of the target as needed.

Enabling Decorators in TypeScript:

Decorators are not enabled by default in TypeScript. You need to enable the experimentalDecorators compiler option in your tsconfig.json file:

{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}

Use Cases for Decorators:

  • Dependency Injection: In frameworks like Angular, decorators are used to define services, components, or routes, making it easy to inject dependencies into classes.
  • Logging: As shown above, decorators can log method calls, parameters, or the results of functions.
  • Validation: In server-side frameworks, decorators can be used to validate input parameters automatically.
  • Access Control: Decorators can modify the behavior of class methods based on conditions like user roles or permissions.

Decorators provide a clean, declarative way of extending and altering the behavior of your code without altering the underlying implementation. However, it’s important to note that they are still an experimental feature in TypeScript and are subject to future changes.

Question: How does TypeScript handle modules and namespaces?

Answer:

TypeScript provides two primary mechanisms for organizing and structuring code: modules and namespaces. Both serve to group related code, but they have different scopes and purposes. Here’s an overview of how TypeScript handles these concepts:


1. Modules in TypeScript

Modules are a way of organizing code into separate files, allowing you to export and import code between those files. They are based on the ES6 (ECMAScript 2015) module system, which is now widely used in JavaScript. In TypeScript, modules are very similar to how JavaScript ES6 modules work.

Key Characteristics of Modules:

  • Encapsulation: Code inside a module is scoped to that module by default. This means that variables, functions, classes, and interfaces defined within a module are not accessible outside unless explicitly exported.
  • Export and Import: To share functionality across different parts of the application, you use export to expose something from a module and import to bring that functionality into another module.

Example:

Module1.ts

// Exporting variables, functions, or classes
export const greet = (name: string) => {
  return `Hello, ${name}!`;
};

export class Person {
  constructor(public name: string) {}
}

Module2.ts

// Importing the exports from Module1
import { greet, Person } from './Module1';

console.log(greet("Alice")); // Hello, Alice!
const person = new Person("Bob");
console.log(person.name);  // Bob

Key Points:

  • Default Exports: You can export a single item as the default export from a module.

    // Default Export
    export default class Car {
      constructor(public make: string, public model: string) {}
    }
  • Named Exports: You can export multiple items and import them by name.

    // Named Exports
    export const calculate = (a: number, b: number) => a + b;
  • Import Syntax: You can import either the default export or named exports from a module:

    import Car from './Car';  // Importing the default export
    import { calculate } from './Math';  // Importing a named export

Modules in TypeScript are always file-based. Each file that uses the import or export keyword is considered a module, and the module system helps avoid global namespace pollution, encouraging modular design.


2. Namespaces in TypeScript

Namespaces (also called “internal modules” in TypeScript) are a way of organizing code within a single file or across multiple files using a logical grouping mechanism. Namespaces were the primary way of organizing code before TypeScript introduced the ES6 module system.

Key Characteristics of Namespaces:

  • Global Scope: Code within a namespace is still in the global scope unless it is enclosed by the namespace itself. In essence, namespaces allow you to create logically grouped code that avoids polluting the global namespace.
  • No Explicit Export/Import: Unlike modules, you do not use import or export within namespaces. The namespace members are available globally once declared, though they are scoped to the namespace.

Example:

namespace Vehicle {
  export class Car {
    constructor(public make: string, public model: string) {}
  }

  export const calculateFuelEfficiency = (miles: number, gallons: number) => {
    return miles / gallons;
  };
}

const myCar = new Vehicle.Car("Toyota", "Camry");
console.log(myCar.make);  // Toyota
console.log(Vehicle.calculateFuelEfficiency(400, 10));  // 40

Key Points:

  • Exporting Members: You can use the export keyword to expose specific members from the namespace, making them accessible outside the namespace.

  • No Importing: Since namespaces are meant to be used within the global scope, you do not need to import them. Once a namespace is defined, its members can be accessed anywhere in the code after the namespace declaration.

    namespace MathUtilities {
      export function add(x: number, y: number) {
        return x + y;
      }
    }
    
    const sum = MathUtilities.add(5, 10);  // Accessing function from a namespace

Differences Between Modules and Namespaces

FeatureModulesNamespaces
ScopeEncapsulated within individual filesGlobal scope, but logically grouped
UsageTypically used for organizing large codebases across filesUsed within a single file or across files but without module syntax
DeclarationUses import and export keywordsUses namespace and export keywords for internal structure
File-basedAlways file-based (every file with import/export is a module)Not file-based (members are global within the namespace)
Best Use CaseLarge applications or libraries, external dependenciesSmaller, legacy applications or when working within a single file

When to Use Modules vs. Namespaces:

  • Modules are the preferred approach in modern TypeScript applications, especially for large-scale applications and when working with external libraries. They align with the ES6 module system and provide clear boundaries between different parts of the application.
  • Namespaces are typically used in legacy codebases or when you’re working with a small amount of code in a single file. They are not as scalable or maintainable as modules in larger projects.

Since the introduction of ES6 modules, TypeScript recommends using modules over namespaces for structuring code. However, namespaces still have use cases, particularly in simpler or older TypeScript projects.

Question: How would you handle third-party JavaScript libraries that don’t have TypeScript definitions?

Answer:

Handling third-party JavaScript libraries that don’t come with TypeScript definitions can be a challenge, but TypeScript provides several ways to work around this. Here’s how you can handle such cases:


1. Check for Existing Type Definitions (DefinitelyTyped)

Before proceeding with any manual solutions, you should first check if type definitions are available through DefinitelyTyped, a repository of TypeScript type definitions for popular JavaScript libraries.

Steps:

  • Search the DefinitelyTyped repository (https://github.com/DefinitelyTyped/DefinitelyTyped) or use npm to see if type definitions are available:

    npm install --save-dev @types/library-name
  • If you find the type definitions, you can install them and TypeScript will automatically recognize the types.

Example:

For the library lodash, type definitions are available via the @types/lodash package:

npm install --save lodash
npm install --save-dev @types/lodash

2. Create Your Own Type Definitions (Custom Typings)

If type definitions are not available, and you’re working with a JavaScript library that doesn’t have any type support, you can create your own TypeScript declaration files.

Steps:

  1. Create a Declaration File: Create a .d.ts file to declare the types for the library. This file should be placed in a folder like src/types or typings.

  2. Declare the Module: Inside the .d.ts file, declare the module using declare module. You can then declare the types for the library’s functionality.

Example:

If you’re using a library like myLibrary without type definitions:

  • Create a myLibrary.d.ts file:
    declare module 'myLibrary' {
      export function doSomething(arg: string): void;
      export const version: string;
    }
  1. Use the Module: Now you can use the library in your code with basic type safety:
import { doSomething, version } from 'myLibrary';

doSomething('Hello');
console.log(version);  // TypeScript now knows that `version` is a string
  1. Declare the Module Globally: If you are working with libraries that don’t use import/export but are added to the global window object (like older JavaScript libraries), you can declare the types in a global namespace.
    declare global {
      interface Window {
        myLibrary: any;
      }
    }

Notes:

  • This approach works well when you need only minimal type definitions for certain functions or properties and don’t want to write extensive type definitions.
  • Use any for functions or objects when you don’t know or want to define the specific types (though this defeats some of the benefits of using TypeScript).

3. Use any or unknown (Last Resort)

If you’re unsure about the types or don’t have the resources to write custom typings, you can fall back on using any or unknown types to bypass TypeScript’s strict typing system.

  • any: This effectively disables type checking for the given variable, which can be useful in certain cases, but you lose all the safety benefits of TypeScript.

    import * as myLibrary from 'myLibrary';
    
    const result: any = myLibrary.someFunction('test');
  • unknown: This is safer than any because it forces you to perform some type-checking before using the value.

    import * as myLibrary from 'myLibrary';
    
    const result: unknown = myLibrary.someFunction('test');
    
    if (typeof result === 'string') {
      // Now TypeScript knows that `result` is a string
      console.log(result.toUpperCase());
    }

Important:

While this is a quick and easy solution, it should be used sparingly because it defeats many of TypeScript’s safety features. Always aim to define types wherever possible.


4. Type Augmentation (For Global Objects)

If you’re working with libraries that augment global objects (like window or document), you can use declaration merging to augment the types of those objects.

Example:

For a library that adds properties to the window object:

// myLibrary.d.ts
declare global {
  interface Window {
    myLibrary: any; // Define the type of the object that is added to the global window
  }
}

// Then, in your code
window.myLibrary.someFunction();

In this case, you augment the existing window interface to include the new library feature.


5. Use TypeScript’s skipLibCheck Option

If the library is large and you don’t want to write custom types or deal with type definition issues, you can set skipLibCheck to true in your tsconfig.json. This will instruct TypeScript to skip type checking for all declaration files (i.e., third-party .d.ts files), potentially avoiding errors due to missing or incorrect types in external libraries.

{
  "compilerOptions": {
    "skipLibCheck": true
  }
}

However, this is generally a workaround rather than a solution and should only be used in situations where you trust the library and don’t need strict type checking for it.


6. Contribute to the Community

If you end up writing your own type definitions, you can contribute them back to the DefinitelyTyped repository. This way, other TypeScript users can benefit from your work.


Summary: Handling JavaScript Libraries Without TypeScript Definitions

  • Search for Existing Definitions: Check DefinitelyTyped or the npm package for existing type definitions (@types/).
  • Write Custom Type Definitions: Create a .d.ts file to define basic or complete types for the library.
  • Use any or unknown: As a last resort, use the any or unknown types to bypass TypeScript’s type checks.
  • Use Type Augmentation: If the library modifies global objects, use declaration merging to augment existing types.
  • Skip Library Checks: If you’re okay with bypassing some type checks, use skipLibCheck in your tsconfig.json.
  • Contribute to DefinitelyTyped: If you write your own type definitions, contribute them to the community.

By using these strategies, you can ensure that third-party JavaScript libraries that lack TypeScript definitions can still be used effectively within your TypeScript project while maintaining as much type safety as possible.

Read More

If you can’t get enough from this article, Aihirely has plenty more related information, such as typescript interview questions, typescript interview experiences, and details about various typescript job positions. Click here to check it out.

Trace Job opportunities

Hirely, your exclusive interview companion, empowers your competence and facilitates your interviews.

Get Started Now