Typescript - rs-hash/Learning GitHub Wiki

Concepts

  • TypeScript is a statically typed superset of JavaScript
  • It provides static type checking and other advantages
  • Type Safety: Catch type-related errors at compile time rather than runtime. This reduces the likelihood of unexpected issues in your code.
  • Early Error Detection: Static type checking helps catch type-related errors at compile-time, which means that many potential issues are identified before the code is executed. This leads to more robust and reliable code.

Add Typescript to Javascript

Introducing TypeScript to an existing JavaScript project can be a valuable step to enhance code quality, maintainability, and developer productivity. Here are step-by-step instructions for adding TypeScript to a JavaScript project:

  1. Install TypeScript:

    If you don't already have TypeScript installed globally, you can do so with npm or yarn:

    npm install -g typescript
    # or
    yarn global add typescript
  2. Initialize a TypeScript Configuration:

    In your project directory, initialize a TypeScript configuration file. You can use tsc --init to generate a tsconfig.json file:

    tsc --init

    This file contains TypeScript compiler options and configuration settings. You can fine-tune it later according to your project's needs.

  3. Configure tsconfig.json:

    Open the generated tsconfig.json file and configure it according to your project's requirements. Common options to consider include the target (JavaScript version to compile to), outDir (output directory for compiled files), and include (files to be compiled). Make sure to specify the outDir to separate your compiled JavaScript from your source TypeScript files.

  4. Convert JavaScript Files to TypeScript:

    Start converting your JavaScript files to TypeScript (.ts) files one at a time. You can do this by renaming the .js files to .ts or creating new .ts files for your code. TypeScript files will coexist with your JavaScript files during this transition.

  5. Add Type Annotations:

    Enhance your TypeScript code by adding type annotations to variables, function parameters, and return types. TypeScript allows you to gradually introduce type information as needed. This is a gradual process, so you don't need to annotate everything at once.

  6. Check for Type Errors:

    Run the TypeScript compiler to check for type errors in your code. You can use the following command to compile your TypeScript code:

    tsc

    The TypeScript compiler will generate JavaScript files in the outDir directory you specified in tsconfig.json.

  7. Resolve Type Errors:

    Address any type errors that the TypeScript compiler reports. This might involve refining type annotations, type assertions, or fixing type-related issues in your code.

  8. Leverage TypeScript Features:

    As you get more comfortable with TypeScript, take advantage of features like interfaces, classes, generics, and other advanced type-checking capabilities to improve your codebase.

Babel and tsc

TypeScript Compiler (tsc): The TypeScript compiler (tsc) is primarily responsible for transpiling TypeScript code into JavaScript. It handles type checking and converts TypeScript-specific syntax and features into equivalent JavaScript code.

Babel: Babel, on the other hand, is a generic JavaScript transpiler that is not specific to TypeScript. It's designed to work with JavaScript and other JavaScript supersets like TypeScript. Babel can transform modern JavaScript features and syntax into older, more widely supported versions of JavaScript (e.g., ECMAScript 5 or earlier). Babel is often used for compatibility purposes, ensuring that JavaScript code can run on a wide range of environments, including older browsers.

Tools like TSLint and ESLint with TypeScript support can analyze code for potential issues and enforce coding standards, which is especially useful in large codebases.

Generic type

// Define a generic type that represents an array with elements of type T
type ArrayType<T> = T[];

// Use the generic type to declare variables with specific element types
const numbers: ArrayType<number> = [1, 2, 3, 4, 5];
const strings: ArrayType<string> = ["apple", "banana", "cherry"];

Using generics in interfaces allows you to create flexible and reusable abstractions for various data types while maintaining type safety.

unknown type

The unknown type in TypeScript represents a type-safe counterpart to the any type. While any allows for completely unrestricted values, unknown is a type that is more restrictive and safer to use. When you have a value of type unknown, you cannot perform arbitrary operations on it without first narrowing down its type. This helps catch type-related errors at compile time.

Here's how unknown works and how to use it:

  1. Type Safety: Variables of type unknown can hold values of any type, but you must explicitly narrow down the type before performing operations on them.

  2. Type Narrowing: To work with unknown values, you can use type guards, type assertions, or other methods to inform TypeScript about the actual type of the value.

  3. Avoiding any: The unknown type is often used in scenarios where you would otherwise use any, but you want to maintain type safety.

Example:

function processValue(value: unknown) {
  if (typeof value === 'string') {
    // Use a type guard to narrow the type
    console.log(value.toUpperCase());
  } else {
    // Without narrowing the type, you can't perform operations
    console.log("Value is not a string");
  }
}

let myValue: unknown;
myValue = "Hello, TypeScript!";
processValue(myValue);

In this example, we define a function processValue that takes an unknown value as a parameter. Inside the function, we use a type guard (typeof value === 'string') to narrow down the type of value before we can safely call toUpperCase() on it. This ensures type safety and helps prevent runtime errors.

Using unknown is a best practice in situations where you want to maintain type safety while working with values of uncertain or dynamic types. It's particularly useful when interacting with data from external sources, like user input or APIs, where you're unsure about the data's actual type.

class vs abstract class

A class can be instantiated, while an abstract class cannot. You use an abstract class when you want to provide a base class for other classes to inherit from but don't want to create instances of the base class itself.

Interface Inheritance & Implementation Inheritance

Interface inheritance (extends) is about defining the shape of an object, while implementation inheritance (implements) is about defining class behavior. A class can implement multiple interfaces, but it can extend only one class.

Implements

Yes, you can use generics in TypeScript interfaces to create parameterized interfaces. Generics in interfaces allow you to define interfaces that can work with a variety of data types. Here's an example of how to use generics in an interface:

// Define a generic interface for a collection
interface Collection<T> {
  add(item: T): void;
  remove(item: T): void;
  contains(item: T): boolean;
}

// Implement the generic interface for a collection of strings
class StringCollection implements Collection<string> {
  private items: string[] = [];

  add(item: string) {
    this.items.push(item);
  }

  remove(item: string) {
    const index = this.items.indexOf(item);
    if (index !== -1) {
      this.items.splice(index, 1);
    }
  }

  contains(item: string) {
    return this.items.includes(item);
  }
}

Conditional type

Conditional types in TypeScript allow you to create type definitions based on conditions. They are particularly useful when you want to perform type transformations or map one type to another based on some condition. Conditional types use the extends keyword to check a type and return a type accordingly. Here's an example to illustrate how conditional types work:

type CheckNumber<T> = T extends number ? 'It is a number' : 'It is not a number';

type Result1 = CheckNumber<42>; // Result1 is 'It is a number'
type Result2 = CheckNumber<'hello'>; // Result2 is 'It is not a number'

never in typescript

The never type represents values that never occur. It's often used to indicate that a function will never return (e.g., it throws an exception).

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

mapped type is in TypeScript.

Mapped types create new types by transforming the properties of an existing type. They are useful for creating variations of types.

type Optional<T> = {
  [K in keyof T]?: T[K];
};

keyof keyword in TypeScript, and how is it used?

keyof is used to get the keys (property names) of an object type. It's often used in mapped types and type manipulation.

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

Type vs Interface

  • interface is for defining object structures and class implementations, while type can represent any valid type.
  • type can be used with union, intersection, and mapped types.
  • Use interface for defining object shapes, and use type for union, intersection, and custom types.

TypeScript is a statically typed superset of JavaScript that enhances JavaScript by adding static types and a few additional features. It was developed by Microsoft and has gained significant popularity for building large-scale JavaScript applications. Below, I'll explain various concepts related to TypeScript in depth:

  1. Type Annotations and Inference:

    • TypeScript allows you to specify the types of variables, function parameters, and return values using type annotations.
    • It also has a type inference system that automatically deduces types when you don't explicitly specify them.
    let name: string = "John"; // Type annotation
    let age = 30; // Type inference, age is inferred as number
  2. Primitive Types:

    • TypeScript supports JavaScript's primitive data types, including number, string, boolean, null, undefined, and symbol.
    let count: number = 42;
    let message: string = "Hello, TypeScript!";
    let isReady: boolean = true;
  3. Custom Types:

    • TypeScript allows you to define custom types using type and interface.
    type Point = { x: number; y: number };
    interface User {
      name: string;
      age: number;
    }
  4. Enums:

    • Enums are a way to define a set of named constant values.
    enum Color {
      Red,
      Green,
      Blue,
    }
    let selectedColor: Color = Color.Red;
  5. Type Assertion:

    • Type assertion is a way to tell TypeScript that a value should be treated as a specific type.
    let value: any = "Hello, TypeScript!";
    let length = (value as string).length;
  6. Functions:

    • TypeScript supports defining functions with parameter and return type annotations.
    function add(x: number, y: number): number {
      return x + y;
    }
  7. Interfaces:

    • Interfaces define the structure of objects, including their properties and methods.
    interface Person {
      name: string;
      age: number;
    }
  8. Classes:

    • TypeScript supports classes, allowing you to define object-oriented structures with constructors, properties, and methods.
    class Car {
      constructor(public make: string, public model: string) {}
    }
  9. Inheritance and Polymorphism:

    • TypeScript supports inheritance and polymorphism, allowing you to create derived classes that inherit from base classes.
    class Animal {
      makeSound() {
        console.log("Animal makes a sound");
      }
    }
    class Dog extends Animal {
      makeSound() {
        console.log("Dog barks");
      }
    }
  10. Generics:

    • Generics enable you to create reusable components and functions that work with a variety of types.
    function identity<T>(arg: T): T {
      return arg;
    }
    let value: string = identity("Hello, Generics!");
  11. Union and Intersection Types:

    • TypeScript allows you to combine types using union (|) and intersection (&) operators.
    type NumberOrString = number | string;
    type Employee = { id: number } & { name: string };
  12. Modules and Namespaces:

    • TypeScript supports modular code organization using import and export statements.
    // Module A
    export function greet(name: string) {
      console.log(`Hello, ${name}!`);
    }
    
    // Module B
    import { greet } from "./moduleA";
    greet("Alice");
  13. Type Guards:

    • TypeScript provides type guards to narrow down types based on runtime checks.
    function isString(value: any): value is string {
      return typeof value === "string";
    }
    if (isString(input)) {
      console.log(input.toUpperCase());
    }
  14. Declaration Files (.d.ts):

    • Declaration files are used to provide type information for libraries and code written in plain JavaScript.

Declaration files in TypeScript, often represented with a .d.ts extension, are used to provide type information for libraries or modules that are written in pure JavaScript or other languages that don't have native TypeScript support. These files help TypeScript understand the types and structures of these external code components.

  1. Decorators:

    • Decorators are used for adding metadata to classes, methods, and properties in TypeScript.
    @sealed
    class Greeter {
      greeting: string;
      constructor(message: string) {
        this.greeting = message;
      }
      @enumerable(false)
      greet() {
        return "Hello, " + this.greeting;
      }
    }
  2. Tooling and IDE Support:

    • TypeScript is well-supported by popular code editors like Visual Studio Code and provides advanced tooling for code analysis, refactoring, and error checking.
  3. Async/Await:

    • TypeScript supports the async/await syntax for handling asynchronous operations with Promises.
    async function fetchData() {
      const response = await fetch("https://api.example.com/data");
      const data = await response.json();
      return data;
    }

These are some of the core concepts and features of TypeScript. TypeScript helps developers catch errors at compile-time, write more maintainable code, and leverage the benefits of static typing while working with JavaScript. It's especially valuable in large-scale projects where code quality and maintainability are crucial.

Interview Questions

Basic

type and interface are used for defining custom data structures, but they have some differences in their capabilities and intended use cases.

Type:

  1. Definition: The type keyword is used to define a custom type in TypeScript.

  2. Primary Use: type is primarily used to define union types, intersection types, and mapped types. It's more versatile when it comes to combining and transforming types.

  3. Composition: You can create complex types by combining existing types using union (|) and intersection (&) operators.

    type Name = string;
    type Age = number;
    type Person = {
      name: Name;
      age: Age;
    };
  4. Mapped Types: You can use type to define mapped types, which allow you to create new types based on the properties of an existing type.

    type Person = {
      name: string;
      age: number;
    };
    
    type ReadonlyPerson = {
      readonly [K in keyof Person]: Person[K];
    };
  5. Extending Types: You can extend existing types by creating new types based on them.

    type Animal = {
      name: string;
    };
    
    type Dog = Animal & {
      breed: string;
    };
  6. Declaration Merging: type supports declaration merging, which means you can add properties to an existing type.

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

Interface:

  1. Definition: The interface keyword is used to define a custom structure that represents the shape of an object.

  2. Primary Use: interface is primarily used for defining the shape of objects and classes in TypeScript. It's intended for describing the structure of data.

  3. Object Shape: It's designed to specify the required and optional properties of an object and their types.

    interface Person {
      name: string;
      age: number;
    }
  4. Extending Interfaces: You can extend interfaces to create new interfaces that inherit properties and methods from existing ones.

    interface Animal {
      name: string;
    }
    
    interface Dog extends Animal {
      breed: string;
    }
  5. Implements for Classes: Interfaces are often used to define the structure that classes must adhere to when implementing them.

    interface Shape {
      area(): number;
    }
    
    class Circle implements Shape {
      // Must implement the 'area' method
      area() {
        return Math.PI * this.radius * this.radius;
      }
      constructor(private radius: number) {}
    }
  6. Declaration Merging: Interfaces also support declaration merging, similar to type.

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

Key Differences:

  1. Compatibility: Interfaces are open for extension and can be implemented by classes. Types, on the other hand, are more flexible and can represent various data structures.

  2. Implements vs. Extends: Interfaces are typically used for defining the shape of objects and classes that implement them. Types are used to create new types by combining existing types or for more complex type transformations.

  3. Declaration Merging: Both type and interface support declaration merging, but type is often preferred when defining mapped types and complex union/intersection types.

In practice, choosing between type and interface depends on your specific use case. If you need to define the shape of objects, use interface. If you need more flexibility in defining types, combining types, or creating mapped types, use type. In many cases, they can be used interchangeably, and the choice may come down to personal preference or project conventions.

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

TypeScript is a statically typed superset of JavaScript. It extends JavaScript by adding static types, allowing developers to explicitly specify variable types and catch type-related errors during development. JavaScript, on the other hand, is dynamically typed, which means that variable types are determined at runtime. TypeScript code is transpiled into JavaScript, making it compatible with all JavaScript environments.

Example:

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

// Transpiled JavaScript code
function greet(name) {
  return `Hello, ${name}!`;
}

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

You can declare a variable with a specific type in TypeScript using type annotations. Simply specify the variable's name, followed by a colon and the desired type.

Example:

let age: number = 30; // Declaring 'age' as a number
let name: string = "Alice"; // Declaring 'name' as a string

3. Explain the benefits of using TypeScript in a project.

Benefits of using TypeScript include:

  • Static Typing: TypeScript provides static type checking, which catches type-related errors at compile-time, leading to more robust and bug-free code.
  • Enhanced Tooling: TypeScript offers better code intelligence, autocompletion, and improved refactoring tools in IDEs like Visual Studio Code.
  • Improved Readability: Type annotations make code more self-documenting and easier to understand, especially in larger projects.
  • Code Quality: TypeScript encourages good coding practices, such as well-defined interfaces and type-safe code, leading to higher code quality.
  • Maintainability: Static types help prevent runtime errors and make codebases easier to maintain and refactor.

4. What are type annotations and type inference in TypeScript?

  • Type Annotations: Type annotations are explicit declarations of variable types using a colon followed by the type name. They provide a clear indication of the expected type for a variable, function parameter, or return value.

    Example:

    let age: number = 30; // Type annotation for 'age'
    function greet(name: string): string { /* ... */ } // Type annotations for function parameters and return type
  • Type Inference: TypeScript's type inference system automatically deduces variable types when you don't explicitly specify them. It analyzes the value assigned to a variable to determine its type.

    Example:

    let age = 30; // Type inference infers 'age' as a number
    function greet(name) { /* ... */ } // Type inference for function parameters and return type

5. Describe the differences between "null" and "undefined" in TypeScript.

  • null: null represents the intentional absence of any object value. It is a value that a variable can have to indicate that it does not point to any object in memory. It is a valid value to assign to a variable explicitly.

    Example:

    let person: string | null = null; // 'person' can hold a string or null
  • undefined: undefined represents the absence of a value, typically when a variable has been declared but not yet assigned a value. It is also the default value of function parameters that have no provided argument.

    Example:

    let age: number | undefined; // 'age' can hold a number or be undefined
    function greet(name?: string) { /* ... */ } // 'name' is optional and can be undefined

In summary, while both null and undefined indicate absence of values, null is often used intentionally to represent absence, whereas undefined often represents uninitialized or missing values. TypeScript allows you to work with these distinctions to write safer code.

Types and Interfaces:

1. What is the difference between the "any" type and "unknown" type in TypeScript?

  • "any" Type: The "any" type in TypeScript allows variables to have values of any type, effectively opting out of type checking. It provides maximum flexibility but minimal type safety.

    let value: any = 42; // 'value' can be any type
  • "unknown" Type: The "unknown" type, on the other hand, represents a type-safe counterpart to "any." Variables of type "unknown" can hold values of any type, but you cannot perform operations on them without type assertions or type checking.

    let userInput: unknown = "Hello, TypeScript!";
    let length = (userInput as string).length; // Type assertion to perform operations

The key difference is that while "any" allows unchecked operations, "unknown" enforces type checks before performing operations, promoting type safety.

2. How do you define custom types in TypeScript using "type" and "interface"?

  • "type": The "type" keyword allows you to create custom types by defining type aliases. Type aliases can represent a wide range of types, including primitive types, object shapes, union types, and more.

    type Point = {
      x: number;
      y: number;
    };
    
    type Status = "active" | "inactive";
  • "interface": Interfaces define the shape of objects, including their properties and methods. They are typically used for defining contracts for classes and objects.

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

Both "type" and "interface" can be used to create custom types, and the choice between them depends on the specific use case. Use "type" for union types, literal types, and more complex type definitions. Use "interface" when defining the structure of objects or for describing class contracts.

3. Explain the use of the "readonly" keyword in TypeScript.

The "readonly" keyword in TypeScript is used to indicate that a property or array element should not be modified after it is assigned a value. It enforces immutability for properties or elements, helping prevent unintended changes.

Example with properties:

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

let point: Point = { x: 10, y: 20 };
// You can read the properties
console.log(point.x, point.y);
// But you can't modify them
point.x = 30; // Error: Cannot assign to 'x' because it is a read-only property

Example with array elements:

let numbers: readonly number[] = [1, 2, 3];
// You can read the elements
console.log(numbers[0]);
// But you can't modify them
numbers[0] = 4; // Error: Index signature in type 'readonly number[]' only permits reading

"readonly" is especially useful when you want to ensure that certain values remain constant or when you're dealing with data that should not change during the runtime of your program.

4. What are union types and intersection types in TypeScript?

  • Union Types: Union types allow a variable to have values of multiple types. You use the "|" (pipe) symbol to specify possible types.

    let value: string | number = "Hello";
    value = 42; // Valid
    value = true; // Error: Type 'boolean' is not assignable to type 'string | number'
  • Intersection Types: Intersection types combine multiple types into a single type. You use the "&" (ampersand) symbol to create an intersection type.

    type Worker = {
      work: () => void;
    };
    
    type Eater = {
      eat: () => void;
    };
    
    type Employee = Worker & Eater;
    
    let employee: Employee = {
      work: () => { /* ... */ },
      eat: () => { /* ... */ },
    };

Union types represent values that can be one of several types, while intersection types represent values that have all the combined types.

5. How can you create optional properties in an interface?

In TypeScript, you can make properties in an interface optional by adding a "?" after the property name.

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

let person1: Person = { name: "Alice" }; // Valid
let person2: Person = { name: "Bob", age: 30 }; // Valid
let person3: Person = { age: 25 }; // Error: Property 'name' is missing

Optional properties are useful when you want to define a set of properties that may or may not be present in an object. This flexibility allows you to work with objects that have varying shapes while still providing type safety.

Functions and Generics:

1. Describe the purpose of type guards in TypeScript and provide an example.

Type guards in TypeScript are used to narrow down the type of a variable within a conditional block, ensuring type safety. They are particularly helpful when working with union types or when dealing with types that have common properties.

Example using the typeof type guard:

function logValue(value: string | number) {
  if (typeof value === "string") {
    console.log(`Value is a string: ${value}`);
  } else {
    console.log(`Value is a number: ${value}`);
  }
}

logValue("Hello"); // Value is a string: Hello
logValue(42);      // Value is a number: 42

2. Explain what generics are in TypeScript and when you would use them.

Generics in TypeScript allow you to write reusable, type-safe code by creating components (functions, classes, interfaces) that can work with various data types. You use placeholders called type parameters to define generics.

Use cases for generics:

  • Writing data structures (e.g., arrays, stacks, linked lists) that can hold different types of values.
  • Creating reusable functions and classes that work with multiple data types.
  • Building libraries and APIs that need to accommodate a variety of input types while ensuring type safety.

Example of a generic function:

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

let result1 = identity("Hello"); // Infers 'string'
let result2 = identity(42);      // Infers 'number'

3. How do you create a generic function in TypeScript?

To create a generic function in TypeScript, you define type parameters in angle brackets (<>) before the function's parameter list. These type parameters act as placeholders for the actual types that the function will work with.

Example of a generic identity function:

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

let result1 = identity("Hello"); // Infers 'string'
let result2 = identity(42);      // Infers 'number'

4. What is function overloading in TypeScript, and why is it useful?

Function overloading in TypeScript allows you to define multiple function signatures for a single function, each specifying different parameter types and return types. It's useful when you want to provide type-safe alternatives for calling a function with different argument combinations.

Example of function overloading:

function greet(name: string): string;
function greet(name: string, age: number): string;

function greet(name: string, age?: number): string {
  if (age === undefined) {
    return `Hello, ${name}!`;
  } else {
    return `Hello, ${name}, you are ${age} years old!`;
  }
}

let greeting1 = greet("Alice");        // Returns 'Hello, Alice!'
let greeting2 = greet("Bob", 30);      // Returns 'Hello, Bob, you are 30 years old!'

Function overloading improves code readability and provides better type checking when dealing with functions that have multiple ways to be called.

5. Describe the differences between "function types" and "callable interfaces" in TypeScript.

  • Function Types: Function types in TypeScript define the shape of a function, including its parameter types and return type. They can be used to annotate variables that are functions.

    type AddFunction = (a: number, b: number) => number;
    
    const add: AddFunction = (a, b) => a + b;
  • Callable Interfaces: Callable interfaces are interfaces that can be implemented by objects to make them callable like functions. They define a call signature within the interface, specifying the parameter types and return type of the callable object.

    interface AddFunction {
      (a: number, b: number): number;
    }
    
    const add: AddFunction = (a, b) => a + b;

The main difference is that function types define the shape of standalone functions, while callable interfaces define the shape of objects that can be invoked as functions. Callable interfaces are useful when you want to create objects that behave like functions, such as custom function-like classes or objects with callable behavior.

Classes and Inheritance:

1. How do you create classes in TypeScript, and how is inheritance achieved?

You create classes in TypeScript using the class keyword. Inheritance is achieved by using the extends keyword to create a subclass (derived class) that inherits properties and methods from a superclass (base class).

Example:

class Animal {
  constructor(name: string) {
    this.name = name;
  }
  name: string;
  makeSound() {
    console.log(`${this.name} makes a sound.`);
  }
}

class Dog extends Animal {
  constructor(name: string, breed: string) {
    super(name); // Calls the superclass constructor
    this.breed = breed;
  }
  breed: string;
  bark() {
    console.log(`${this.name} (a ${this.breed}) barks.`);
  }
}

const dog = new Dog("Buddy", "Golden Retriever");
dog.makeSound(); // Outputs: Buddy makes a sound.
dog.bark();      // Outputs: Buddy (a Golden Retriever) barks.

2. What is the "super" keyword used for in TypeScript classes?

The super keyword is used in a derived class constructor to call the constructor of its superclass. It allows you to initialize the inherited properties and perform additional setup.

3. Explain the concept of abstract classes and methods in TypeScript.

An abstract class in TypeScript is a class that cannot be instantiated on its own but serves as a blueprint for other classes (derived classes). Abstract classes can have abstract methods, which must be implemented by any derived class. They are used to enforce a common structure among derived classes.

Example:

abstract class Shape {
  abstract area(): number; // Abstract method
}

class Circle extends Shape {
  constructor(private radius: number) {
    super();
  }
  area() {
    return Math.PI * this.radius ** 2;
  }
}

const circle = new Circle(5);
console.log(circle.area()); // Outputs: 78.53981633974483

4. How can you achieve method overriding in TypeScript?

Method overriding in TypeScript occurs when a subclass provides its own implementation of a method that is already defined in its superclass. To achieve method overriding, simply define a method with the same name and parameters in the subclass.

5. What is the difference between "public," "private," and "protected" access modifiers in TypeScript?

  • public: Members marked as public can be accessed from anywhere, both inside and outside the class.

  • private: Members marked as private can only be accessed within the class in which they are defined. They are not accessible from outside the class or its derived classes.

  • protected: Members marked as protected can be accessed within the class and its derived classes but not from outside the class hierarchy.

Modules and Namespaces:

1. What is a TypeScript module, and how does it differ from a namespace?

  • Module: A TypeScript module is a way to organize and encapsulate code into separate files, making it easier to manage dependencies and maintainability. Modules use the export and import keywords to define and import components from other files. Modules are typically used in modern TypeScript and are compatible with ES6 modules.

  • Namespace: A TypeScript namespace is an older way to organize code into logical groupings within a single global scope. It uses the namespace keyword to define namespaces. While namespaces are still supported, modules are generally preferred for structuring code.

2. How do you import and export modules in TypeScript?

To export members (variables, functions, classes) from a module, you use the export keyword. To import these members in another module, you use the import keyword.

Example of exporting and importing a module:

// Module A
export const x = 42;

// Module B
import { x } from "./moduleA";
console.log(x); // Outputs: 42

3. What is the purpose of the "namespace" keyword in TypeScript, and when would you use it?

The namespace keyword in TypeScript is used to define a logical grouping of code within a single global scope. Namespaces can be used to organize related code into a separate namespace to avoid naming conflicts. However, namespaces have largely been replaced by the more modern module system in TypeScript, and they are not commonly used in modern TypeScript projects.

## TypeScript Compiler and Configuration:

1. What is the TypeScript Compiler (tsc), and how do you use it to compile TypeScript files?

The TypeScript Compiler (tsc) is a command-line tool provided by TypeScript to compile TypeScript code into JavaScript code. You can install TypeScript globally (npm install -g typescript) and then run tsc followed by the name of the TypeScript file you want to compile.

Example:

tsc myfile.ts

2. What is a tsconfig.json file, and what are its key settings?

A `tsconfig.json` file is a configuration file used to specify settings and options for the TypeScript compiler (tsc). It allows you to define compiler options, file inclusion/exclusion patterns, and other project-related settings. Some key settings include:

- `target`: Specifies the ECMAScript target version (e.g., "ES5" or "ES6").
- `outDir`: Defines the output directory for compiled JavaScript files.
- `include` and `exclude`: Specify file patterns to include or exclude from compilation.
- `strict`: Enables strict type-checking options (e.g., `strictNullChecks`, `strictFunctionTypes`).

3. Explain the significance of "strict" mode in TypeScript's tsconfig.json.

The `"strict"` mode in TypeScript's `tsconfig.json` is a group of compiler options that enable a set of strict type-checking rules. It includes options like `"strictNullChecks"` and `"strictFunctionTypes"` to enforce type safety and reduce common sources of bugs. Using `"strict"` mode is recommended to write safer and more maintainable TypeScript code.

Advanced Topics:

Project and Real-World Scenarios:

1. How would you migrate an existing JavaScript project to TypeScript incrementally?

Migrating an existing JavaScript project to TypeScript can be done incrementally by following these steps:

a. Start by renaming your .js files to .ts or .tsx (for React projects).

b. Enable TypeScript gradually by adding type annotations to functions, variables, and objects as needed.

c. Utilize TypeScript's any type for parts of your codebase that are difficult to type or require more effort.

Example:

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

// Incremental TypeScript migration
function add(a: number, b: number): number {
  return a + b;
}

2. Describe your experience with handling third-party libraries and declaration files in TypeScript.

When using third-party JavaScript libraries in TypeScript, you can create or find declaration files (.d.ts) that provide type information for those libraries. If declaration files don't exist, you can create them yourself to provide type safety and code completion.

Error Handling and Debugging:

1. How does TypeScript help catch type-related errors during development?

TypeScript provides static type checking, which means it catches type-related errors at compile time rather than runtime. This helps prevent common runtime errors and ensures that code is more predictable and easier to maintain.

Example of a type-related error caught by TypeScript:

// TypeScript error: Argument of type 'true' is not assignable to parameter of type 'string'.
function greet(name: string) {
  console.log(`Hello, ${name}`);
}

greet(true); // Error at compile time, not at runtime

TypeScript Best Practices:

1. What are some best practices for writing clean and maintainable TypeScript code?

Some best practices for writing clean and maintainable TypeScript code include:

  • Use meaningful variable and function names.
  • Utilize TypeScript's static typing to catch errors early.
  • Write clear and concise comments and documentation.
  • Follow coding style guidelines, such as using consistent indentation and naming conventions.
  • Break down complex code into smaller, reusable functions or classes.
  • Organize code into modules and follow module import/export patterns.

2. How do you ensure type safety when working with external APIs or data sources in TypeScript?

To ensure type safety when working with external APIs or data sources, you can:

  • Create TypeScript interfaces that match the expected data structure of the API responses.
  • Use TypeScript's type annotations and type assertions to explicitly specify data types.
  • Validate and transform incoming data to match the expected types.

Example with an external API response:

interface ApiResponse {
  id: number;
  name: string;
  age: number;
}

// Fetch data from an API and ensure type safety
fetch('https://api.example.com/data')
  .then((response) => response.json())
  .then((data: ApiResponse) => {
    // TypeScript ensures 'data' has the expected shape
    console.log(data.name);
  })
  .catch((error) => {
    console.error(error);
  });

By following these practices, TypeScript helps ensure that your code remains type-safe and easier to maintain, especially when working with external data sources or APIs.

complex TypeScript interview questions

Type System:

1. Explain the differences between structural typing and nominal typing in TypeScript. How does TypeScript's type system work in this regard?

Structural typing focuses on the shape of types, meaning that if two types have the same structure, they are considered compatible, regardless of their names. Nominal typing, on the other hand, relies on type names for compatibility.

Example:

// Structural typing
type Person = { name: string };
type Employee = { name: string };
const person: Person = { name: "Alice" };
const employee: Employee = person; // Works because of structural typing

// Nominal typing
type Person = { name: string };
type Employee = { name: string } & { _brand: "employee" };
const person: Person = { name: "Alice" };
const employee: Employee = person; // Error: Type 'Person' is not assignable to type 'Employee'

2. What are mapped types in TypeScript, and how can you use them to create new types based on existing ones? Provide examples.

Mapped types allow you to create new types based on existing ones by transforming each property in the original type.

Example:

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

type ReadonlyPerson = {
  readonly [K in keyof Person]: Person[K];
};

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

3. Describe the key features of conditional types in TypeScript and provide use cases where they are beneficial.

Conditional types in TypeScript allow you to create types that depend on a condition.

Example:

type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">; // true
type B = IsString<42>;       // false

Use cases:

  • Conditional rendering based on type.
  • Building generic utility types like Exclude and Extract.

4. TypeScript introduces "keyof" and "typeof" operators. How can these operators be used to create more type-safe code? Provide examples.

The keyof operator returns a union type of all keys in an object, while typeof operator allows you to infer the type of a value.

Example:

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

type PersonKeys = keyof typeof person; // "name" | "age"

5. How do you leverage TypeScript's "template literal types" to create complex type transformations? Provide an example illustrating their usefulness.

Template literal types allow you to create types that are based on template string patterns.

Example:

type Color = "red" | "blue";
type Size = "small" | "medium" | "large";

type Styling = `${Color}-${Size}`;

const buttonStyle: Styling = "red-small"; // Valid
const invalidStyle: Styling = "green-medium"; // Error

Advanced Types and Generics:

1. What are type inference rules for function return types in TypeScript? Explain the "infer" keyword and its use in function return type inference.

TypeScript uses type inference to automatically determine the return type of a function based on its implementation. The infer keyword is often used in conjunction with conditional types to infer and capture types.

Example:

function firstElement<T extends any[]>(arr: T): T extends [] ? undefined : T[0] {
  return arr.length === 0 ? undefined as any : arr[0];
}

const result = firstElement([1, 2, 3]); // Inferred as 'number'
const emptyResult = firstElement([]);    // Inferred as 'undefined'

In this example, the firstElement function returns the first element of an array if it exists. The use of conditional types and infer allows TypeScript to infer the return type as either the type of the first element or undefined.

2. Discuss TypeScript's support for variadic tuple types. Provide an example illustrating the use of rest and spread operators with tuples.

TypeScript supports variadic tuple types, which allow you to define functions that operate on tuples of varying lengths.

Example:

function concatArrays<T extends unknown[]>(
  ...arrays: T[]
): T extends (infer U)[] ? U[] : never {
  return ([] as any[]).concat(...arrays);
}

const result = concatArrays([1, 2], ["a", "b"], [true, false]);
// 'result' is inferred as: (string | number | boolean)[]

In this example, the concatArrays function takes variadic arrays and concatenates them into a single array while preserving the types. The use of conditional types and infer helps infer the resulting type as a union of all element types.

3. Explain how you can use the "never" type in TypeScript, and provide scenarios where it is useful.

The never type represents a value that never occurs. It is useful in scenarios where you want to indicate that something should never happen or when narrowing types.

Example 1: Indicating unreachable code.

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

const result: string = throwError("An error occurred"); // Error: Function has no return statement

In this example, the throwError function indicates that it never returns a value because it throws an error unconditionally.


In this example, the `never` type is used to ensure that all possible cases are handled in a switch statement, making the code type-safe.

**4. TypeScript supports recursive type definitions. Describe how you can create a type that represents a nested hierarchy of objects, such as a tree structure.**

Recursive types are created by referencing the type itself within its own definition. You can represent a nested hierarchy, like a tree structure, using recursive types.

Example:
```typescript
type TreeNode<T> = {
  value: T;
  children?: TreeNode<T>[];
};

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

In this example, the TreeNode type is defined recursively, allowing you to represent a tree structure where each node may have children.

5. What are "conditional expressions" in TypeScript? How can they be applied to create types that depend on runtime values? Provide examples.

Conditional types in TypeScript allow you to create types that depend on runtime values or other types. They are based on conditional expressions using the ? and : operators.

Example:

type CheckType<T> = T extends string
  ? "It's a string"
  : T extends number
  ? "It's a number"
  : "It's something else";

const strResult: CheckType<string> = "It's a string";
const numResult: CheckType<number> = "It's a number";
const boolResult: CheckType<boolean> = "It's something else";

In this example, the CheckType type depends on the runtime type of its generic parameter. Depending on the type passed, it provides different type annotations.

Real-World Scenarios:

Certainly! Here are answers to the questions about handling TypeScript in real-world scenarios:

1. Describe your experience with handling type definitions for external JavaScript libraries in TypeScript. How do you create and maintain declaration files for such libraries?

Handling type definitions for external JavaScript libraries is a common task when working with TypeScript. Here's how you can do it:

Creating Declaration Files:

  1. Use Existing Declaration Files: Many popular libraries already have declaration files available on DefinitelyTyped (https://definitelytyped.org/). You can install them using npm or yarn, and TypeScript will automatically pick up the types.

    Example:

    npm install --save @types/lodash
  2. Manual Typings: If declaration files are not available, you can create your own by writing TypeScript typings for the library. These declarations typically have a .d.ts extension.

    Example:

    // my-library.d.ts
    declare module 'my-library' {
      export function someFunction(): void;
      // Add more declarations as needed
    }

Maintaining Declaration Files:

  1. Regular Updates: Keep your declaration files up to date with the library's versions. Monitor updates to the library and update the typings accordingly.

  2. Community Contributions: Contribute to DefinitelyTyped or open-source projects to share your typings with the community and receive updates from others.

  3. TypeScript's "strict" Mode: Enabling TypeScript's strict mode (strict: true in tsconfig.json) helps catch type-related issues in your code and declarations.

  4. Declaration Merging: You can extend or merge existing declaration files with your own to enhance them or add missing typings.

2. Explain how TypeScript interfaces can be used to define the contract between the front end and back end of a web application. Discuss potential challenges and solutions.

TypeScript interfaces play a crucial role in defining contracts between front-end and back-end components of a web application. Here's how they can be used:

Defining Contracts:

  • Front-End Interfaces: Create TypeScript interfaces to define the structure of data sent and received by the front end, including API responses, form models, and state shapes.

    Example:

    interface User {
      id: number;
      name: string;
      email: string;
    }
  • Back-End Interfaces: Similarly, define interfaces for data expected by the back end, such as request payloads, database models, and API routes.

    Example:

    interface CreateUserRequest {
      name: string;
      email: string;
    }

Potential Challenges:

  • Version Mismatch: Front-end and back-end interfaces might not always align due to version mismatches. Backward-compatible changes and versioning strategies can help mitigate this issue.

  • Complex Data Structures: Handling complex data structures, nested objects, and arrays in interfaces can be challenging. You can use TypeScript's utility types like Partial, Pick, and Record to simplify complex type definitions.

  • Async Operations: Handling asynchronous operations and API responses can be tricky. Promises, async/await, and error handling are essential in these scenarios.

3. In a large-scale TypeScript project, how would you ensure consistent type definitions and maintainable code as the codebase grows? Discuss best practices and tools you would use.

Best Practices:

  1. Use Strict Mode: Enable TypeScript's strict mode (strict: true in tsconfig.json) to catch type-related issues at compile time.

  2. Modularization: Organize your code into modules and follow a modular architecture. Use namespaces or ES6 modules to encapsulate related functionality.

  3. Code Reviews: Implement a code review process to ensure that code adheres to coding standards and follows best practices.

  4. Documentation: Document your code, especially complex or critical sections. Use JSDoc comments to provide type information and descriptions.

  5. Linting and Formatting: Use tools like ESLint and Prettier to enforce coding standards, consistent formatting, and catch common errors.

  6. Testing: Write unit tests, integration tests, and end-to-end tests to validate code correctness and type safety.

  7. Version Control: Use version control systems like Git to track changes, collaborate with a team, and manage codebase versions.

Tools:

  1. TSLint or ESLint with TypeScript Support: Use linters with TypeScript support to enforce coding standards and catch common issues.

  2. TypeScript Type Checker: Run the TypeScript type checker (tsc) as part of your build process to ensure type safety.

  3. IDE Support: Use IDEs like Visual Studio Code with TypeScript support, which provides real-time type checking, autocompletion, and debugging.

  4. Continuous Integration (CI): Set up CI pipelines that run tests, type checking, and linting on every code commit.

  5. Package Managers: Leverage package managers like npm or yarn to manage dependencies and ensure consistency.

  6. TypeScript Declaration Generator: Tools like dtslint can automatically generate declaration files for your code, ensuring that external consumers have access to type information.

These advanced TypeScript interview questions are designed to assess your deep understanding of TypeScript's type system, advanced type features, decorators, and your ability to solve complex real-world problems with TypeScript. Be prepared to provide detailed explanations and examples during the interview.

⚠️ **GitHub.com Fallback** ⚠️