Skip to content

Latest commit

 

History

History
3814 lines (2610 loc) · 148 KB

typescript.md

File metadata and controls

3814 lines (2610 loc) · 148 KB

TypeScript

What is typescript and why should we use it?
TypeScript is a superset of JavaScript, which means that it includes all the features of JavaScript, plus some additional features, such as:

  • Type safety: TypeScript allows developers to specify the types of data that variables and functions can hold. This helps to catch errors early in the development process and prevents bugs from being introduced into the code.
  • Classes and interfaces: TypeScript provides support for classes and interfaces, which are fundamental concepts in object-oriented programming. This makes it easier to write and maintain large and complex JavaScript applications.
  • Modern JavaScript features: TypeScript supports all the latest JavaScript features, such as modules, generics, and arrow functions. This allows developers to take advantage of the most powerful features of JavaScript without having to worry about compatibility issues.

There are many reasons why developers should use TypeScript. Some of the key benefits include:

  • Improved code quality: TypeScript's type system helps to improve the quality of JavaScript code by catching errors early and preventing bugs from being introduced. This can lead to faster development times and more reliable software.
  • Better developer experience: TypeScript provides many features that make it easier and more enjoyable to write JavaScript code. For example, type checking can help developers to identify and fix errors quickly and easily.
  • Increased scalability: TypeScript's support for classes and interfaces makes it easier to write and maintain large and complex JavaScript applications. This makes TypeScript a good choice for developing enterprise-grade software.

Overall, TypeScript is a powerful language that can help developers to write better, more reliable, and more scalable JavaScript code.


What are the benefits of typescript?
TypeScript offers a number of benefits over JavaScript, including:

  • Improved code quality: TypeScript's type system helps to catch errors early in the development process, preventing bugs from being introduced into the code. This can lead to faster development times and more reliable software.
  • Better developer experience: TypeScript provides many features that make it easier and more enjoyable to write JavaScript code. For example, type checking can help developers to identify and fix errors quickly and easily.
  • Increased scalability: TypeScript's support for classes and interfaces makes it easier to write and maintain large and complex JavaScript applications. This makes TypeScript a good choice for developing enterprise-grade software.

In addition to these general benefits, TypeScript also offers a number of specific advantages, such as:

  • Improved tooling support: TypeScript is well-supported by popular IDEs and text editors, providing features such as code completion, error checking, and debugging.
  • Better collaboration: TypeScript's type system can help to improve collaboration between developers by making code more readable and understandable.
  • Easier onboarding: TypeScript can make it easier for new developers to get on board with a project by providing clear documentation of the codebase.

Overall, TypeScript is a valuable tool for any JavaScript developer who wants to write better, more reliable, and more scalable code.

Here are some examples of how TypeScript can be used to improve the quality of JavaScript code:

  • Prevent common errors: TypeScript can help to prevent common errors such as passing the wrong type of argument to a function or trying to access a property that does not exist.
  • Make code more readable and maintainable: TypeScript can be used to document the types of data that variables and functions can hold, making code more readable and maintainable.
  • Refactor code more safely: TypeScript can help to refactor code more safely by ensuring that changes to the code do not break existing functionality.

If you are developing a large and complex JavaScript application, or if you simply want to improve the quality of your code, I highly recommend using TypeScript.


Explain generics in typescript?
Generics in TypeScript are a way to create functions, classes, and interfaces that can work with a variety of data types. This makes code more reusable and flexible.

Generics are defined using type parameters. Type parameters are placeholders that can be replaced with any type when the generic function, class, or interface is used.

Here is an example of a generic function:

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

This function can be used with any data type, such as numbers, strings, or objects. For example, you could use it to identity a number:

const number = 10;
const identityNumber = identity(number); // identityNumber is of type number

Or you could use it to identity a string:

const string = "Hello, world!";
const identityString = identity(string); // identityString is of type string

Generics can also be used to define generic classes and interfaces. For example, here is a generic class:

class Stack<T> {
  private items: T[] = [];

  push(item: T) {
    this.items.push(item);
  }

  pop(): T {
    return this.items.pop();
  }

  isEmpty(): boolean {
    return this.items.length === 0;
  }
}

This class can be used to create stacks of any data type. For example, you could create a stack of numbers:

const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.push(3);

const poppedNumber = numberStack.pop(); // poppedNumber is of type number

Or you could create a stack of strings:

const stringStack = new Stack<string>();
stringStack.push("Hello");
stringStack.push("world!");

const poppedString = stringStack.pop(); // poppedString is of type string

What are modules in typescript?
Modules in TypeScript are a way to organize and encapsulate code. They allow you to export specific parts of your code, so that they can be used by other modules. This can help to improve the modularity and maintainability of your code.

Modules are defined using the export and import keywords. The export keyword is used to export specific parts of your code, such as functions, classes, and variables. The import keyword is used to import specific parts of other modules into your code.

For example, here is a simple module that exports a single function:

// my-math.ts

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

This module can be imported into other modules using the import keyword:

// my-app.ts

import { add } from './my-math';

const sum = add(1, 2); // sum is of type number

Modules can also be used to export entire classes and interfaces. For example, here is a module that exports a simple Person class:

// person.ts

export class Person {
  name: string;

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

This module can be imported into other modules using the import keyword:

// my-app.ts

import { Person } from './person';

const person = new Person('John Doe'); // person is of type Person

List the built-in types in typescript?
The built-in types in TypeScript are:

  • number: Represents a numeric value, such as 1, 2.3, or 10.2e5.
  • string: Represents a sequence of characters, such as "Hello, world!".
  • boolean: Represents a truth value, either true or false.
  • void: Represents the absence of a value.
  • null: Represents the intentional absence of a value.
  • undefined: Represents the absence of a value due to uninitialization.
  • any: Represents any type of value.
  • object: Represents an object, which is a collection of properties.
  • symbol: Represents a unique identifier.

TypeScript also has a number of other built-in types, such as arrays, tuples, enums, and unions. These types are derived from the basic built-in types listed above.


How to call base class constructor from child class in typescript?
To call the base class constructor from a child class in TypeScript, you use the super() keyword. The super() keyword refers to the parent class of the current class.

For example, the following code shows how to call the base class constructor from a child class:

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

class Employee extends Person {
  constructor(public name: string, public salary: number) {
    super(name);
  }
}

const employee = new Employee("John Doe", 100000);

In the above example, the Employee class extends the Person class. The Employee class constructor calls the super() keyword to call the Person class constructor. This ensures that the Employee class object is initialized with all the properties of the Person class.

You can also pass arguments to the super() keyword to initialize the base class constructor parameters. For example, the following code shows how to pass arguments to the base class constructor:

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

class Employee extends Person {
  constructor(public name: string, public salary: number) {
    super(name, 30);
  }
}

const employee = new Employee("John Doe", 100000);

In the above example, the Employee class constructor passes the name argument to the super() keyword to initialize the name property of the Person class. The Employee class constructor also passes the value 30 to the super() keyword to initialize the age property of the Person class.

It is important to note that you must always call the super() keyword in the child class constructor if you want to initialize the base class constructor parameters. If you do not call the super() keyword, the base class constructor will not be called and the base class properties will not be initialized.


Do we need to compile typescript files and why?
Yes, TypeScript files need to be compiled before they can be run in a browser or on a server. This is because TypeScript is a superset of JavaScript, and browsers and servers only understand JavaScript.

When you compile a TypeScript file, the TypeScript compiler converts the TypeScript code into JavaScript code. This JavaScript code can then be run in a browser or on a server.


How to perform string interpolation in typescript?
To perform string interpolation in TypeScript, you use template literals. Template literals are enclosed in backticks (`), and they allow you to embed expressions within the string.

For example, the following code shows how to perform string interpolation using a template literal:

const name = "John Doe";
const age = 30;

const greeting = `Hello, ${name}! You are ${age} years old.`;

You can also use template literals to perform more complex string interpolation tasks, such as formatting numbers and dates. For example, the following code shows how to format a number using a template literal:

const price = 10.99;

const formattedPrice = `The price is $${price.toFixed(2)}`;

What is the difference between .ts and .tsx file extensions in typescript?
The .ts file extension is used for TypeScript files that contain plain TypeScript code. The .tsx file extension is used for TypeScript files that contain JSX code.

JSX is a JavaScript syntax extension that allows you to write HTML-like code within your JavaScript code. This can make it easier to develop user interfaces in React and other JavaScript frameworks.

If you are developing a TypeScript application that does not use JSX, you should use the .ts file extension. If you are developing a TypeScript application that uses JSX, you should use the .tsx file extension.


What are decorators in typescript?
Decorators in TypeScript are a way to add metadata to classes, methods, properties, and parameters. Metadata is data that describes the code, but it is not executed. Decorators can be used to perform a variety of tasks, such as:

  • Adding annotations: Decorators can be used to add annotations to code, such as documentation or version information.
  • Modifying the behavior of code: Decorators can be used to modify the behavior of code, such as logging method calls or validating property values.
  • Creating custom code generation: Decorators can be used to create custom code generation, such as generating documentation or creating proxy objects.

Decorators are defined using a special syntax that consists of the @ symbol followed by the name of the decorator function. The decorator function can take any number of arguments, which can be used to provide metadata to the code.

For example, the following code shows a simple decorator that logs method calls:

function logMethodCall(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log(`Calling method ${propertyKey}`);
}

@logMethodCall
class MyClass {
  public myMethod() {}
}

When the MyClass.myMethod() method is called, the logMethodCall() decorator will be executed and the method call will be logged to the console.

Decorators can also be used to modify the behavior of code. For example, the following code shows a decorator that validates property values:

function validateProperty(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  if (descriptor.value !== 'foo') {
    throw new Error(`Property ${propertyKey} must be equal to 'foo'`);
  }
}

@validateProperty
class MyClass {
  public myProperty: string;
}

When a new instance of the MyClass class is created, the validateProperty() decorator will be executed and the myProperty property value will be validated. If the property value is not equal to 'foo', an error will be thrown.


what is interface in typescript?
An interface in TypeScript is a way to define a contract in your code. It defines the shape of an object, with specified properties and methods.

Here are some key things to know about interfaces in TypeScript:

  • An interface is defined using the interface keyword:
interface User {
  name: string;
  id: number;
}
  • An interface doesn't implement anything - it just defines what properties and methods something should have.

  • Interfaces can describe objects, functions, arrays, classes, etc.

  • Interfaces are used for static type checking - they ensure an object meets the requirements of the interface.

  • Objects that implement an interface must contain the properties and methods defined in the interface.

  • Interfaces can extend other interfaces using the extends keyword.

  • Interfaces can't be instantiated directly - they exist only as a type definition.

  • Implementing classes must follow the interface contract - if they don't, it will result in a compile error.

So in summary, interfaces in TypeScript allow you to define reusable static types that define a contract within your code. They are useful for static analysis and enable type checking during development.


What is the difference between classes and interfaces in typescript?
Classes and interfaces are two important concepts in TypeScript. Classes are used to define blueprints for objects, while interfaces are used to define the structure of objects.

  • Classes: Classes can be used to define the properties and methods of objects. Classes can also be used to inherit from other classes to create new classes with new features.
  • Interfaces: Interfaces define the structure of objects, but they do not provide any implementation. This means that interfaces cannot be used to create objects directly. Interfaces are typically used to validate the types of objects that are passed to functions and methods.

How to implement class constants in typescript?
There are two ways to implement class constants in TypeScript:

  1. Using the static and readonly keywords
  2. Using the const keyword with the as const assertion

Using the static and readonly keywords

The first way to implement class constants in TypeScript is to use the static and readonly keywords. The static keyword makes the constant available on the class itself, rather than on instances of the class. The readonly keyword prevents the constant from being reassigned.

For example, the following code shows how to implement a class constant using the static and readonly keywords:

class Person {
  static readonly MAX_AGE = 120;

  constructor(public name: string) {}
}

// Access the class constant
const maxAge = Person.MAX_AGE;

The MAX_AGE constant is now available on the Person class itself. This means that you can access the constant without having to create an instance of the class.

Using the const keyword with the as const assertion

The second way to implement class constants in TypeScript is to use the const keyword with the as const assertion. The const keyword prevents the variable from being reassigned. The as const assertion tells the TypeScript compiler to treat the variable as a constant.

For example, the following code shows how to implement a class constant using the const keyword with the as const assertion:

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

// Access the class constant
const MAX_AGE: number as const = 120;

The MAX_AGE constant is now treated as a constant by the TypeScript compiler. This means that you cannot reassign the constant.


What are getters and setters in typescript?
Getters and setters are methods that allow you to read and write to the properties of an object in a controlled way. Getters are used to get the value of a property, while setters are used to set the value of a property.

Getters

Getters are defined using the get keyword. Getters can take any number of arguments, and they can return any value.

For example, the following code shows a simple getter:

class Person {
  private _name: string;

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

  get name(): string {
    return this._name;
  }
}

This getter returns the value of the _name property.

Getters can be used to perform validation and other tasks before returning the value of a property. For example, the following getter validates the value of the _age property before returning it:

class Person {
  private _age: number;

  constructor(age: number) {
    if (age < 0) {
      throw new Error('The age must be a non-negative number.');
    }

    this._age = age;
  }

  get age(): number {
    return this._age;
  }
}

This getter will throw an error if the _age property is less than zero.

Setters

Setters are defined using the set keyword. Setters take one argument, which is the new value of the property.

For example, the following code shows a simple setter:

class Person {
  private _name: string;

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

  set name(name: string) {
    this._name = name;
  }
}

This setter sets the value of the _name property to the value of the name parameter.

Setters can be used to perform validation and other tasks before setting the value of a property. For example, the following setter validates the value of the _age property before setting it:

class Person {
  private _age: number;

  constructor(age: number) {
    this._age = age;
  }

  set age(age: number) {
    if (age < 0) {
      throw new Error('The age must be a non-negative number.');
    }

    this._age = age;
  }
}

This setter will throw an error if the age parameter is less than zero.

Benefits of using getters and setters

Getters and setters offer a number of benefits, including:

  • Validation: Getters and setters can be used to perform validation on the values of properties. This can help to prevent errors in your code.
  • Encapsulation: Getters and setters can be used to encapsulate the properties of an object. This can help to protect the properties from being modified directly.
  • Flexibility: Getters and setters can be used to implement custom logic when reading and writing to the properties of an object. This can make your code more flexible and reusable.

Does typescript support all object-oriented principles?
Yes, TypeScript supports all the core object-oriented principles, including:

  • Abstraction: TypeScript supports abstract classes and interfaces, which can be used to define the structure and behavior of objects without providing a concrete implementation. This allows you to create reusable and flexible code.
  • Encapsulation: TypeScript supports private and protected members, which can be used to protect the internal state of objects from being modified directly. This helps to ensure the correctness and maintainability of your code.
  • Inheritance: TypeScript supports inheritance, which allows you to create new classes that inherit the properties and methods of existing classes. This allows you to reuse code and create more complex and powerful objects.
  • Polymorphism: TypeScript supports polymorphism, which allows you to treat different types of objects in the same way. This allows you to write more generic and reusable code.

In addition to these core principles, TypeScript also supports a number of other object-oriented features, such as:

  • Decorators: Decorators can be used to add metadata to classes, methods, and properties. This metadata can be used to modify the behavior of code at runtime.
  • Generics: Generics allow you to write code that can be used with different types of data. This makes your code more flexible and reusable.
  • Modules: Modules allow you to organize your code into separate units. This makes your code more modular and easier to maintain.

When to use interfaces and when to use classes in typescript?
Interfaces and classes are both powerful tools for object-oriented programming in TypeScript. However, they have different purposes and are best used in different situations.

When to use interfaces:

  • To define the shape of an object, without providing any implementation.
  • To validate the types of objects passed to functions and methods.
  • To create generic types that can be used with different types of data.

When to use classes:

  • To define the structure and behavior of objects.
  • To implement abstract classes and interfaces.
  • To create new classes that inherit from existing classes.

How could you check null and undefined in typescript?
We can use the nullish coalecsing operator as follows:

function nullOrUndefinedDetector(input: any) {
  console.log(`Input value is ${input ?? "null or undefined"}`);
}

nullOrUndefinedDetector(null);          // "Input value is null or undefined"
nullOrUndefinedDetector(undefined);     // "Input value is null or undefined"
nullOrUndefinedDetector(0);             // "Input value is 0"
nullOrUndefinedDetector("0");           // "Input value is 0"
nullOrUndefinedDetector("");            // "Input value is "
nullOrUndefinedDetector({ a: 1 });      // "Input value is [object Object]"
nullOrUndefinedDetector([1, 2, 3]);     // "Input value is 1,2,3"
nullOrUndefinedDetector(true);          // "Input value is true"
nullOrUndefinedDetector(false);         // "Input value is false"

What are the differences between the typescript and javascript?
TypeScript and JavaScript are both programming languages, but they have some key differences.

TypeScript is a superset of JavaScript, which means that all valid JavaScript code is also valid TypeScript code. TypeScript adds optional static typing to JavaScript, which can help to catch errors early and make code more maintainable.

JavaScript is a dynamically typed language, which means that the types of variables and expressions are not checked until runtime. This can make it easier to write code quickly, but it can also lead to errors that are difficult to debug.

Here is a table that summarizes the key differences between TypeScript and JavaScript:

Feature TypeScript JavaScript
Static typing Optional Dynamic
Type safety Higher Lower
Compiler Yes No
IDE support Excellent Good

Overall, TypeScript is a good choice for developers who want to write JavaScript code that is more type-safe and maintainable.


What is a typescript map file?
A TypeScript map file is a file that contains information about the original TypeScript source code for a compiled JavaScript file. This information can be used to debug TypeScript code and to improve the performance of JavaScript applications.

TypeScript map files are generated by the TypeScript compiler when you compile your TypeScript code to JavaScript. The map file contains information about the following:

  • The original TypeScript source code for each line of JavaScript code.
  • The types of variables and expressions in the original TypeScript source code.
  • The locations of type annotations in the original TypeScript source code.

TypeScript map files can be used to debug TypeScript code by allowing you to step through the original TypeScript source code as you step through the compiled JavaScript code. This can be helpful for understanding how TypeScript types are translated to JavaScript code.


Could we use typescript on backend and how?
Yes, TypeScript can be used on the backend. In fact, TypeScript is becoming increasingly popular for backend development.

There are a few different ways to use TypeScript on the backend. One way is to use a Node.js framework that supports TypeScript, such as Express.js or Nest.js. These frameworks provide TypeScript support out of the box, so you can start using TypeScript right away.


What is the difference between types String and string in typescript?
The difference between the types String and string in TypeScript is that String is a wrapper object, while string is a primitive type.

Wrapper objects are objects that wrap around primitive types and provide additional functionality. For example, the String object provides methods for manipulating strings, such as charAt(), indexOf(), and substring().

Primitive types are the basic building blocks of TypeScript types. They include numbers, strings, booleans, and undefined.

In general, it is recommended to use the string type instead of the String type, unless you need to use the additional functionality provided by the String object. This is because primitive types are more efficient and have fewer associated overhead costs.


What is Type Erasure in TypeScript?
In TypeScript, type erasure refers to the process of removing type annotations during compilation. This means that the extra information you provide about the data types of variables, functions, and other elements in your TypeScript code gets stripped away when it's converted into JavaScript.


How can we create an enum with string values?

enum Color {
  Red = "RED",
  Green = "GREEN",
  Blue = "BLUE",
}

console.log(Color.Red);
console.log(Color.Green);
console.log(Color.Blue);

What does the pipe(|) mean in typescript?
The pipe (|) symbol in TypeScript itself doesn't directly relate to function composition, unlike some other languages. However, it is used to define union types.

A union type describes a variable that can hold one of several different types. The pipe (|) separates each type in the union.

Here's an example:

let value: string | number;

value = "hello"; // Ok, value is a string
value = 10;       // Ok, value is a number

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

In this example:

  • We define a variable value with a union type string | number. This means value can be either a string or a number, but not both at the same time.
  • Assigning a string or a number to value is valid.
  • Assigning a boolean value results in a compilation error because it's not one of the allowed types in the union.

Union types provide flexibility in your code by allowing a variable to hold different data types depending on the situation.


Describe what are conditional types in typescript?
Conditional types in TypeScript are like "if" statements for types. They let you choose between two types based on a condition that checks another type. This makes your code more flexible and type-safe.

Think of it like this:

// Basic structure:
T extends U ? X : Y 

// Example:
type MyType<T> = T extends string ? string[] : number[];

let strArray: MyType<string>; // strArray will be of type string[]
let numArray: MyType<number>; // numArray will be of type number[] 

How to make arrays that can only be read in typescript?
There are two primary ways to create read-only arrays in TypeScript:

1. Using ReadonlyArray<T>:

This approach defines the array as a generic ReadonlyArray type, preventing modifications after initialization.

const readOnlyArray: ReadonlyArray<number> = [1, 2, 3];

// Error: Property 'push' does not exist on type 'readonly number[]'
readOnlyArray.push(4);

2. Using readonly Keyword:

This method applies the readonly modifier to each array element, prohibiting changes to individual values.

const readOnlyArray: readonly number[] = [1, 2, 3];

// Error: Cannot assign to '0' because it is a read-only property.
readOnlyArray[0] = 4; 

How can we use optional chaining in typescript?
Optional chaining in TypeScript provides a safe way to access nested object properties without causing errors if a property is null or undefined.

How to use it:

Instead of writing a chain of if statements to check for null or undefined values, use the question mark operator (?.) between properties and method calls.

Example:

interface User {
  address?: {
    street?: string;
  };
}

const user: User = {};

// Without optional chaining
if (user.address && user.address.street) {
  console.log(user.address.street); 
}

// With optional chaining
console.log(user.address?.street); // No error, logs undefined

What is optional chaining in typescript?
Optional chaining (?.) in TypeScript is a safe way to access potentially undefined or null properties of an object.

In short: It prevents ugly errors when you're unsure if a property exists. Instead of crashing, it gracefully returns undefined.

Example:

const user = {
  name: "Alice",
  // address object might not always exist
  address: { 
    street: "Wonderland Lane"
  }
};

// Without optional chaining: 
// const street = user.address.street; // Potential error if user.address is undefined

// With optional chaining:
const street = user.address?.street; // Safe, returns "Wonderland Lane" or undefined

What is the purpose of Nullish Coalescing Operator in typescript?
The Nullish Coalescing Operator (??) in TypeScript provides a safe way to access a value from a variable that might be null or undefined.

In short, it offers a default value when dealing with potentially missing data, but only if the data is actually missing (null or undefined), unlike the traditional OR operator (||) which considers other falsy values like 0 or "" as well.

This leads to more predictable and reliable code when handling optional values.


What are assertion functions in typescript?
Assertion functions in TypeScript are a special type of function used to tell the TypeScript compiler to treat an argument as a specific type at compile time only. They don't perform any runtime type checking.

In short: They're like type "hints" for the compiler, helping it understand your code better and catch potential errors during development, but they disappear entirely in the final JavaScript code.

Example:

function assertIsNumber(value: any): asserts value is number {
  if (typeof value !== 'number') {
    throw new Error('Value must be a number');
  }
}

const input: any = "hello"; 

assertIsNumber(input); // TypeScript now knows that 'input' is a number

// ... you can now safely use 'input' as a number
console.log(input + 5); 

Which access modifiers are implied when not specified in typescript?
In TypeScript, when no access modifier is specified, the implied access modifier is public.

This means that without any explicit modifier, class members (properties and methods) are accessible from anywhere, both inside and outside the class they are defined in.


What is Typings in typescript?
Typings in TypeScript act like instruction manuals for your JavaScript code. They tell TypeScript what data types your variables, function parameters, and return values should have.

In a nutshell:

  • Typings = Data Type Definitions
  • They make your code more reliable and easier to understand.
  • TypeScript uses typings to catch errors early on.

What is the default access modifier for members of a class in typescript?
In TypeScript, the default access modifier for class members (properties and methods) is public. This means that unless you explicitly specify a different access modifier, all members will be accessible from anywhere in your code.


How typescript is optionally statically typed language?
It seems there's a slight misunderstanding. TypeScript isn't "optionally" statically typed; it's inherently statically typed. However, its type system is flexible and allows for implicit type inference, which might give the impression of optional typing. Let me clarify.

Statically Typed: TypeScript analyzes your code for type errors before runtime. This means it checks if variables, function arguments, and return values are used consistently with their declared types during the compilation process.

Type Inference: You don't always have to explicitly write types in TypeScript. The compiler is smart enough to infer the type of a variable based on its initial value or usage context.

Flexibility: While TypeScript encourages type annotations, it allows you to omit them when desired or when you want more dynamic behavior. However, this comes with trade-offs, as you lose the safety net of strict type checking in those instances.

Let's illustrate with an example:

// Explicit type annotation
let name: string = "Alice"; 

// Type inferred automatically
let age = 30;   // TypeScript infers 'age' to be of type 'number'

// No type annotation (implied 'any' type)
let anything = "hello";
anything = 42;  // No type error, could lead to issues later

Explanation:

  1. Explicit Type: We declared name as a string. TypeScript will enforce that only string values can be assigned to name.
  2. Type Inference: We didn't specify a type for age, but TypeScript infers it to be number based on the initial value 30.
  3. Implicit 'any': When you don't provide a type and TypeScript can't infer it, the variable is implicitly typed as any. This is where the "optional" feeling might come from. any bypasses type checking and allows any value to be assigned.

Important Note: While using any or omitting types might seem convenient, it makes your code more prone to runtime errors. TypeScript's strength lies in its type system, and leveraging it fully leads to more robust and maintainable code.


How to use external plain javascript libraries in typescript?
TypeScript offers great flexibility for integrating external JavaScript libraries, even those without built-in type definitions. Here's a breakdown of the process and an illustrative example:

1. Installation (If Necessary)

If the JavaScript library isn't already part of your project, install it using your preferred package manager:

npm install library-name 
# or
yarn add library-name

2. Understanding Type Definitions

TypeScript leverages type definitions (.d.ts files) to understand the structure and types within JavaScript libraries.

  • Option A: Existing Definitions - Many popular libraries have community-maintained type definitions on DefinitelyTyped. You can install these using:

    npm install --save-dev @types/library-name
    # or
    yarn add -D @types/library-name
  • Option B: Creating Definitions - If type definitions aren't available, you can write your own or use the declare keyword to provide basic typing information.

3. Importing and Using the Library

  • Direct Import (with Type Definitions)

    import * as libraryName from 'library-name'; 
    
    libraryName.someFunction(); 
  • Import with require (for global libraries)

    const libraryName = require('library-name');
    
    libraryName.someFunction();
  • Accessing Global Objects (if the library exposes them)

    window.libraryName.someFunction(); 

Does typescript support function overloading?
Yes, TypeScript supports function overloading. Function overloading allows you to define multiple function signatures for the same function name. This enables you to create more flexible and type-safe functions that can handle different input types.

How it works:

  1. Multiple Signatures: You define two or more function signatures with the same name but different parameter lists (number of parameters or their types).
  2. Type Inference: TypeScript's type system analyzes the arguments passed to the overloaded function during compilation.
  3. Signature Matching: It selects the most specific signature that matches the provided arguments.
  4. Implementation: You provide a single implementation for the function, which should handle all the defined overloads.

Example:

function greet(person: string): string;
function greet(person: { name: string; age: number }): string;

function greet(person: unknown): string {
  if (typeof person === 'string') {
    return `Hello, ${person}!`;
  } else if (typeof person === 'object' && person !== null && 'name' in person) {
    return `Hello, ${person.name}! You are ${person.age} years old.`;
  } else {
    return 'Hello, there!';
  }
}

console.log(greet('Alice')); // Output: "Hello, Alice!"
console.log(greet({ name: 'Bob', age: 30 })); // Output: "Hello, Bob! You are 30 years old."

Benefits of Function Overloading:

  • Improved Type Safety: TypeScript ensures that you call the overloaded function with arguments that match one of the defined signatures.
  • Code Readability: Overloading makes your code more concise and easier to understand by clearly showing the different ways a function can be used.
  • Flexibility: You can handle different data types and variations in your function arguments without resorting to multiple function names or complex logic within a single function body.

Explain how and why we could use property decorators in typescript?
Property decorators in TypeScript provide a concise and elegant way to add metadata or modify the behavior of class properties. Think of them as powerful annotations that can enhance your code's functionality without cluttering the core logic.

Why Use Property Decorators?

  1. Clean Separation of Concerns: They separate cross-cutting concerns, such as validation, logging, or reactivity, from the core property definition, making your code more maintainable and readable.
  2. Code Reusability: Define common logic once as a decorator and apply it to multiple properties across your codebase.
  3. Enhanced Metadata: Decorators can attach valuable information (metadata) to properties, which can be leveraged by other parts of your application or libraries.

How Property Decorators Work:

A property decorator is simply a function that is applied to a class property using the @decoratorName syntax. This function receives metadata about the decorated property, allowing you to inspect or modify it.

Example: Building a Validation Decorator

Let's imagine you want to enforce that a specific property in your class should always be a string and have a minimum length. We can achieve this elegantly using a property decorator:

// Decorator factory: Creates the actual decorator function
function MinLength(minLength: number) {
  return function (target: any, propertyKey: string) {
    let value: string;

    // Define a getter and setter for the property
    Object.defineProperty(target, propertyKey, {
      get: () => value,
      set: (newValue: string) => {
        if (newValue.length < minLength) {
          throw new Error(`Property '${propertyKey}' must be at least ${minLength} characters long.`);
        }
        value = newValue;
      },
    });
  };
}

class Product {
  @MinLength(5) // Apply the decorator with desired length
  name: string;

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

// Usage
const validProduct = new Product("Valid Name"); 

const invalidProduct = new Product("Inv"); // Throws an error

Explanation:

  1. Decorator Factory (MinLength):

    • Takes minLength as input and returns the actual decorator function.
    • This pattern allows you to create decorators with configurable parameters.
  2. Decorator Function:

    • Receives the target (class prototype) and propertyKey (name of the decorated property).
    • Uses Object.defineProperty to redefine the property with a custom getter and setter.
    • The setter enforces the minLength validation rule.
  3. Applying the Decorator (@MinLength(5)):

    • Attaches the MinLength decorator to the name property of the Product class, enforcing a minimum length of 5 characters.

What are different components of typescript?
Let's break down the core components of TypeScript and illustrate them with examples.

Fundamental TypeScript Components

  1. Types: The heart and soul of TypeScript. They provide static typing to your JavaScript code, meaning you declare what kind of data a variable can hold. This leads to:

    • Early Error Detection: TypeScript catches type errors during compilation, preventing runtime surprises.
    • Code Clarity and Maintainability: Types act as self-documenting code, making it easier to understand and work with.

    Example:

    let username: string = "Alice"; // String type
    let age: number = 30;          // Number type
    let isLoggedIn: boolean = true; // Boolean type
    
    // Error: Type 'number' is not assignable to type 'string'.
    username = 123; 
  2. Interfaces: Define the structure or shape of an object, ensuring consistency in how you work with data.

    Example:

    interface User {
        id: number;
        name: string;
        email?: string; // Optional property
    }
    
    let newUser: User = {
        id: 1,
        name: "Bob"
    };
  3. Classes: A blueprint for creating objects, encapsulating data (properties) and functions (methods) that work with that data. TypeScript enhances JavaScript classes with features like access modifiers (public, private, protected) and interfaces.

    Example:

    class Animal {
        protected name: string;
    
        constructor(name: string) {
            this.name = name;
        }
    
        makeSound(sound: string): void {
            console.log(`${this.name} makes a ${sound} sound!`);
        }
    }
    
    class Dog extends Animal { 
        constructor(name: string) {
            super(name);
        }
    
        bark(): void {
            this.makeSound("bark"); 
        }
    }
    
    let myDog = new Dog("Buddy");
    myDog.bark(); // Output: Buddy makes a bark sound!
  4. Generics: Provide a way to write reusable code that can work with different types without sacrificing type safety.

    Example:

    function identity<T>(arg: T): T {
        return arg;
    }
    
    let myString: string = identity<string>("Hello");
    let myNumber: number = identity<number>(100); 
  5. Enums: A way to give more meaningful names to sets of numeric values.

    Example:

    enum Status {
        Pending,
        Approved,
        Rejected 
    }
    
    let orderStatus: Status = Status.Approved;
  6. Modules: Allow you to organize your code into smaller, reusable units, making it more maintainable. You use import and export to work with modules.

    Example:

    // mathUtils.ts
    export function add(x: number, y: number): number {
        return x + y;
    }
    
    // app.ts
    import { add } from './mathUtils';
    let sum = add(5, 10);

Key Benefits of Using TypeScript

  • Improved Code Quality: Enforces type safety, reducing runtime errors and bugs.
  • Enhanced Developer Productivity: Provides better code completion, navigation, and refactoring in IDEs.
  • Large-Scale Development: Makes it easier to manage and scale complex projects.

Let me know if you'd like a deeper dive into a specific component or want to explore more advanced TypeScript concepts!


How can we allow class defined in a module to be accessible outside of that module in typescript?
Certainly, let's explore how to make TypeScript classes defined within modules accessible externally.

Fundamentals

In TypeScript, modules serve as containers for code, enhancing organization and preventing naming collisions. By default, members (classes, functions, interfaces, etc.) within a module are internally scoped, meaning they aren't visible or usable outside that module. To grant external access, we utilize the export keyword.

Example

Let's imagine we have a module named shapes.ts:

// shapes.ts
export class Rectangle {
  constructor(public width: number, public height: number) {}

  getArea(): number {
    return this.width * this.height;
  }
}

In this module:

  1. We define a class Rectangle.

  2. Crucially, we prefix the class declaration with export. This keyword signals that the Rectangle class should be accessible from other parts of our project.

Now, let's use this class in another file, say app.ts:

// app.ts
import { Rectangle } from './shapes'; // Assuming 'shapes.ts' is in the same directory

const myRectangle = new Rectangle(5, 10);
console.log(myRectangle.getArea()); // Output: 50

What are the differences between Private and Protected variables in typescript?
Let's break down the differences between private and protected variables in TypeScript:

Private Variables

  • Scope: A private member is only accessible within the same class where it's declared. Not even subclasses (children) can access it directly.
  • Encapsulation: Private members enforce strong encapsulation, hiding data and implementation details from the outside world. This makes your code more maintainable because changes to private members are less likely to break other parts of your codebase.

Protected Variables

  • Scope: A protected member is accessible within the same class where it's declared and by any subclasses (children) that inherit from that class.
  • Inheritance: Protected members facilitate code reuse through inheritance. Subclasses can directly access and potentially modify these members, enabling specialized behavior.

Example

class Animal {
  private name: string; // Private: Only accessible within Animal
  protected sound: string; // Protected: Accessible within Animal and its subclasses

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

  makeSound() {
    console.log(`${this.name} says ${this.sound}`); 
  }
}

class Dog extends Animal {
  constructor(name: string) {
    super(name, 'Woof!'); // Accessing protected 'sound' from parent
  }

  // Example of a subclass-specific method
  fetch() {
    console.log(`${this.name} runs to fetch!`); // Accessing protected 'name' from parent 
  }
}

const myDog = new Dog('Buddy');
myDog.makeSound(); // Output: Buddy says Woof!
myDog.fetch();     // Output: Buddy runs to fetch!

// console.log(myDog.name);    // Error: 'name' is private and only accessible within Animal
// console.log(myDog.sound);  // Error: 'sound' is protected and only accessible within Animal and its subclasses

In essence:

  • Use private when you want to completely hide data and behavior within a single class.
  • Use protected when you want to enable code reuse through inheritance, allowing subclasses to access and potentially extend the behavior defined by the parent class.

What is the difference between enum and const enum in typescript?
Let's break down the difference between regular enum and const enum in TypeScript:

Regular enum

  • Compilation: Regular enums are compiled into JavaScript objects that hold both the key (enum member name) and value (numeric or string representation) mappings. This means they exist as data structures at runtime.
  • Runtime Presence: They have a runtime presence, affecting your JavaScript bundle size.
  • Reverse Mapping: They support reverse mapping, meaning you can get the enum member name from its value.

Example (Regular enum):

enum Status {
  Pending,
  Approved,
  Rejected
}

const requestStatus = Status.Approved; 

console.log(requestStatus); // Output: 1
console.log(Status[1]);     // Output: "Approved" (Reverse mapping)

const enum

  • Compilation: const enums are completely removed by the TypeScript compiler during the transpilation process. They are inlined as their literal values wherever they are used.
  • Runtime Absence: They have no runtime impact, resulting in smaller JavaScript output.
  • No Reverse Mapping: They do not support reverse mapping because the enum itself doesn't exist at runtime.

Example (const enum):

const enum Sizes {
  Small = 'S',
  Medium = 'M',
  Large = 'L'
}

const mySize = Sizes.Medium;

console.log(mySize); // Output: 'M' (directly replaced at compile time)
// console.log(Sizes[mySize]); // Error: Cannot index type 'typeof Sizes'.

In Essence

Think of const enum as a way to introduce strongly-typed constants that are entirely erased during compilation. They are primarily used for performance optimization, especially when you have a large number of enums or don't require runtime presence.

When to Use Which

  • Regular enum: Use when you need reverse mapping (getting the name from the value) or when you might need to iterate over the enum keys/values at runtime.
  • const enum: Use when you prioritize minimizing JavaScript bundle size and don't need runtime representation or reverse mapping of your enum.

Let me know if you'd like a more in-depth explanation of a specific aspect or have any other questions!


Why do we need abstract keyword for classes and their methods in typescript?
Let's break down the concept of abstract classes and methods in TypeScript:

Why Abstract Classes?

  • Enforcing Structure: Abstract classes act as blueprints. They define a common structure (properties and methods) that concrete subclasses must follow. This ensures consistency and helps prevent errors from missing implementations.

  • Representing Incomplete Concepts: Sometimes you have a concept that's too general to be instantiated on its own. An abstract class allows you to represent this idea, leaving the specifics for subclasses.

  • Polymorphism: Even though abstract classes can't be instantiated, you can use them for type checking and polymorphism. This means you can work with different subclasses through a common abstract base type.

Why Abstract Methods?

  • Deferred Implementation: Abstract methods are placeholders within an abstract class. They declare a method signature but provide no actual implementation. The responsibility to provide the logic lies with the concrete subclasses.

  • Customization in Subclasses: This forces each subclass to provide its own specific implementation for the abstract method, tailoring the behavior to its unique purpose.

TypeScript Example

abstract class Shape {
  constructor(public name: string) {}

  // Abstract method (must be implemented by subclasses)
  abstract calculateArea(): number; 

  displayInfo(): void {
    console.log(`Shape: ${this.name}`);
    console.log(`Area: ${this.calculateArea()}`);
  }
}

class Circle extends Shape {
  constructor(public radius: number) {
    super("Circle"); // Call the parent class's constructor
  }

  // Implementation for the abstract method
  calculateArea(): number {
    return Math.PI * this.radius * this.radius;
  }
}

class Rectangle extends Shape {
  constructor(public width: number, public height: number) {
    super("Rectangle");
  }

  // Implementation for the abstract method
  calculateArea(): number {
    return this.width * this.height;
  }
}

// Using abstract class and polymorphism
const shapes: Shape[] = [new Circle(5), new Rectangle(4, 6)];

shapes.forEach((shape) => {
  shape.displayInfo();
});

What is Structural Typing in typescript?
Imagine you're trying to herd a group of animals. You don't really care what specific breed each one is, as long as they all behave like ducks (walk like a duck, quack like a duck). That's the essence of structural typing, often referred to as "duck typing" in other languages.

In TypeScript, structural typing means that type compatibility is determined by the shape of the object, focusing on its properties and methods rather than explicit type declarations.

Here's a simple example:

interface Flyable {
  fly(): void;
}

class Bird {
  fly() {
    console.log("The bird is flying!");
  }
}

class Plane {
  fly() {
    console.log("The plane is soaring!");
  }
}

function takeFlight(obj: Flyable) {
  obj.fly();
}

const parrot = new Bird();
const airbus = new Plane();

takeFlight(parrot); // Output: The bird is flying!
takeFlight(airbus); // Output: The plane is soaring!

Explanation:

  1. We define an interface Flyable which requires a fly() method.
  2. We have two classes, Bird and Plane, both implementing a fly() method, but without explicitly stating that they implement the Flyable interface.
  3. The takeFlight function accepts any object that has a fly() method, thanks to the Flyable type constraint.
  4. Both parrot (a Bird) and airbus (a Plane) can be passed to takeFlight because they structurally match the Flyable interface by having the fly() method, even though they are different classes.

What is Mixin Class in typescript?
Certainly, let's delve into the realm of mixin classes in TypeScript.

What is a Mixin Class?

In essence, a mixin is a design pattern in TypeScript (and other languages) that allows you to add properties and methods from one class to another without directly using inheritance. Think of it like adding ingredients to a recipe – each ingredient contributes its flavor without being a complete dish on its own.

Key Characteristics of Mixins:

  1. Composition over Inheritance: Mixins promote composition over classical inheritance. Instead of creating a complex inheritance hierarchy, you can "mix in" desired functionalities from various mixin classes.

  2. Flexibility: They provide flexibility in assembling classes with specific features. You can pick and choose the mixins you need, leading to more modular and maintainable code.

  3. Code Reuse: Mixins foster code reuse by encapsulating reusable functionalities that can be shared across different classes.

Example:

Let's say we want to create classes representing different types of vehicles. Some vehicles have engines, while others are electric. We can use mixins to model this:

class Vehicle {
  constructor(public name: string) {}
}

class EngineMixin {
  startEngine() {
    console.log("Engine started!");
  }
}

class ElectricMixin {
  chargeBattery() {
    console.log("Battery charging...");
  }
}

// Applying mixins using intersection types
class Car extends Vehicle implements EngineMixin {
  // Implement EngineMixin methods if needed
  startEngine: () => void;
}
applyMixins(Car, [EngineMixin]); // Applying the mixin

class ElectricCar extends Vehicle implements ElectricMixin {
  // Implement ElectricMixin methods if needed
  chargeBattery: () => void;
}
applyMixins(ElectricCar, [ElectricMixin]); // Applying the mixin

// Helper function to apply mixins
function applyMixins(derivedCtor: any, baseCtors: any[]) {
  baseCtors.forEach((baseCtor) => {
    Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
      if (name !== "constructor") {
        Object.defineProperty(
          derivedCtor.prototype,
          name,
          Object.getOwnPropertyDescriptor(baseCtor.prototype, name)!
        );
      }
    });
  });
}

const myCar = new Car("Sedan");
myCar.startEngine(); 

const myElectricCar = new ElectricCar("Tesla");
myElectricCar.chargeBattery(); 

Explanation:

  1. Vehicle Class: A basic class for all vehicles.

  2. EngineMixin and ElectricMixin: These are our mixin classes. They contain functionalities (methods) related to engines and electric systems.

  3. Applying Mixins: We use a helper function applyMixins to apply the mixins to our concrete classes (Car and ElectricCar). This function essentially copies the properties and methods from the mixin prototypes onto the prototypes of our classes.

  4. Intersection Types (implements): We use implements with the mixin class names to ensure type safety. This signals to TypeScript that the concrete classes should have the methods declared in the mixins.

Advantages of Mixins:

  • Improved code organization and reusability.
  • Avoidance of deep and complex inheritance chains.
  • Enhanced flexibility in composing class behavior.

Caveats:

  • Can make it harder to understand the exact prototype chain of an object.
  • Potential for method name collisions if not managed carefully.

What is unique symbol in typescript and tell me its use cases?
There isn't a single "unique symbol" in TypeScript. Instead, TypeScript leverages the concept of Symbols from ECMAScript 2015 (ES6) to provide truly unique identifiers within your code.

What Makes Symbols Special?

  • Guaranteed Uniqueness: Every time you create a Symbol, it's absolutely unique, even if you use the same descriptive string.
  • Hidden from Iteration: Symbols don't appear in for...in loops or Object.keys(). This helps you create "private" object properties.

Use Cases

  1. Preventing Accidental Collisions:

    Imagine you're building a library and want to add properties to objects without the risk of conflicting with other libraries or user code. Symbols solve this problem.

    const MY_KEY = Symbol('mySpecialKey');
    
    class MyClass {
        [MY_KEY]() { 
            return 'This is a hidden method!';
        }
    }
    
    const instance = new MyClass();
    
    console.log(instance[MY_KEY]()); // Output: This is a hidden method!
    
    // The symbol key is protected
    for (const key in instance) {
        console.log(key); // This won't log "MY_KEY"
    }
  2. Well-Known Symbols (Built-in Functionality):

    JavaScript itself uses Symbols to represent special internal properties. TypeScript provides type definitions for these, enabling type safety when working with built-in behaviors.

    class MyIterable {
        private values = [1, 2, 3];
    
        [Symbol.iterator]() {
            let i = 0;
            return {
                next: () => {
                    if (i < this.values.length) {
                        return { value: this.values[i++], done: false };
                    } else {
                        return { done: true };
                    }
                }
            };
        }
    }
    
    const iterable = new MyIterable();
    
    for (const value of iterable) {
        console.log(value); // Outputs: 1, 2, 3
    }

Key Points

  • Descriptive Strings: While not mandatory, it's good practice to give your Symbols descriptive strings (Symbol('myKey')) for debugging.
  • Retrieving Symbol Properties: You can access a property associated with a Symbol using the bracket notation (object[mySymbol]) only if you have a reference to the Symbol itself.

In Essence

Symbols are your go-to for generating truly unique identifiers, especially when you need to avoid naming conflicts or implement "private" properties in your TypeScript projects.


How to make a read-only tuple type in typescript?
TypeScript doesn't have a direct "read-only tuple" type in the same way it has ReadonlyArray. However, you can achieve immutability for tuples using a combination of:

  • readonly Keyword: This keyword prevents modification of individual elements within the tuple.
  • as const Assertion: This assertion infers the most specific type possible, including the exact length and types of elements within the tuple, further solidifying its structure.

Example & Explanation:

// Declare a read-only tuple
const point = [10, 20] as const; 

// Try to modify elements (will result in errors)
point[0] = 5;  // Error: Cannot assign to '0' because it is a read-only property.
point.push(30); // Error: Property 'push' does not exist on type 'readonly [10, 20]'.

// Access elements (allowed)
console.log(point[0]);  // Output: 10

// Type of 'point' is inferred as 'readonly [10, 20]'
type Point = typeof point; 

Explanation:

  1. const point = [10, 20] as const;:

    • We declare point using const, which prevents reassignment of the entire tuple variable.
    • as const is crucial: without it, TypeScript would infer point as number[]. The assertion tells TypeScript to treat the array literal as a tuple with specific element types and length.
  2. Error on Modification Attempts:

    • Trying to change point[0] or use array methods like push results in compile-time errors, as TypeScript enforces the read-only nature.
  3. Type Inference:

    • The type of point becomes readonly [10, 20]. This means it's a tuple where:
      • The first element is always the number 10.
      • The second element is always the number 20.
      • No further elements can be added or removed.

Explain project references in typescript and its benefits?
Project References are a powerful feature in TypeScript that allow you to split your codebase into smaller, more manageable projects. Each project can be developed and compiled independently while still maintaining type-safe dependencies between them. This promotes code reusability, maintainability, and faster build times.

How it Works:

Imagine you have two TypeScript projects: "my-library" and "my-app". "my-library" contains reusable utility functions, while "my-app" is the main application that wants to use them.

  1. tsconfig.json in "my-library":

    • You compile "my-library" normally, generating a declaration file (my-library.d.ts) that describes the public API of your library.
    • You may optionally set "declaration": true in the compilerOptions to automatically generate declaration files during compilation.
  2. tsconfig.json in "my-app":

    • You add a "references" array to the compilerOptions, specifying the path to "my-library"'s tsconfig.json.
    • You use import statements to bring in modules from "my-library" just like any other dependency.

Example:

my-library/src/utils.ts:

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

my-library/tsconfig.json:

{
  "compilerOptions": {
    "outDir": "./dist",
    "declaration": true // Generate declaration files
  },
  "include": ["src/**/*"]
}

my-app/src/index.ts:

import { greet } from 'my-library';

const message = greet('World');
console.log(message);

my-app/tsconfig.json:

{
  "compilerOptions": {
    "outDir": "./dist",
    "references": [
      { "path": "../my-library" } 
    ]
  },
  "include": ["src/**/*"]
}

Benefits of Project References:

  • Modularization: Decompose large projects into smaller, focused units.
  • Code Reusability: Share common logic across different projects.
  • Maintainability: Easier to understand, debug, and maintain smaller codebases.
  • Faster Builds: TypeScript compiler can process each project independently and only recompile affected code, reducing build times especially in larger projects.
  • Improved Collaboration: Different teams can work on separate projects with well-defined interfaces.

What are the use cases for const assertion in typescript?
const assertions in TypeScript provide a way to tell the compiler that a particular variable should be treated as a constant with a literal type, even if its initial value might suggest otherwise. This has some key use cases:

1. Inferring More Precise Types:

TypeScript's type inference can sometimes be too broad. const assertions help narrow down types for better type safety and code clarity.

Example:

// Without const assertion:
const colors = ['red', 'green', 'blue'];
// Type: readonly string[]

// With const assertion:
const colors = ['red', 'green', 'blue'] as const;
// Type: readonly ["red", "green", "blue"]

Here, without the const assertion, colors is typed as readonly string[], meaning you can access any string element, but you can't modify the array. With the const assertion, the type becomes a tuple with specific string literal types. Now, colors[0] is type 'red', not just string.

2. Enforcing Immutability:

const assertions ensure that the variable and its properties (for objects) cannot be reassigned, preventing accidental modifications.

Example:

const car = {
  brand: 'Toyota',
  model: 'Camry',
} as const;

// Error: Cannot assign to 'brand' because it is a read-only property
car.brand = 'Honda';

The const assertion makes car and its properties immutable, leading to an error when trying to modify the brand property.

3. Working with Readonly Data:

When dealing with data from external sources (APIs, configuration files), using const assertions signals that this data is read-only and should not be modified within your code.

Example:

// Simulating data from an API
const apiResponse = {
  status: 'success',
  data: { userId: 123, username: 'johndoe' },
} as const;

// Accessing data:
const userId = apiResponse.data.userId;
// Type of userId: 123 (not just number)

Here, the const assertion clarifies that apiResponse shouldn't be mutated. Additionally, it allows us to use userId with the precise type 123.


How to choose between never, unknown and any in typescript?
Let's break down the differences between never, unknown, and any in TypeScript, along with guidelines on when to use each.

1. never

  • Meaning: Represents values that will never occur.
  • Use Cases:
    • Functions that always throw errors: If a function's sole purpose is to throw an error and never return normally, its return type is never.
    • Exhaustive conditional checks: When you have a type narrowing system where TypeScript can guarantee all possible cases are handled.

Example:

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

function handleValue(x: string | number): string {
  if (typeof x === 'string') {
    return x.toUpperCase(); 
  } else if (typeof x === 'number') {
    return x.toFixed(2); 
  } else {
    // This block is unreachable, so 'x' is narrowed to 'never'
    let unreachableValue: never = x; 
    return unreachableValue; // This line will never be executed
  }
}

2. unknown

  • Meaning: Represents a value whose type is unknown at compile time.
  • Use Cases:
    • Data from external sources: When receiving data from APIs, user input, or other unpredictable sources.
    • Type-safe fallbacks: When you need a more restrictive alternative to any that enforces type checking.

Example:

function parseData(input: unknown): number | string | undefined {
  if (typeof input === 'string') {
    return input.toUpperCase();
  } else if (typeof input === 'number') {
    return input * 2;
  } 
  return undefined; // Or throw an error for invalid input
}

const userDataFromAPI: unknown = await fetchUserData(); 
// Error: Cannot access property 'name' on type 'unknown'
// console.log(userDataFromAPI.name); 

if (typeof userDataFromAPI === 'object' && userDataFromAPI !== null && 'name' in userDataFromAPI) {
  // Now you can access 'name' safely
  console.log(userDataFromAPI.name); 
}

3. any

  • Meaning: The "escape hatch" of TypeScript. It bypasses type checking entirely.
  • Use Cases (Use Sparingly!):
    • Migrating JavaScript code: When you're gradually adding TypeScript to an existing JavaScript project.
    • Interfacing with highly dynamic libraries: In rare cases where a library relies heavily on runtime type manipulation.
  • Caution: Overusing any defeats the purpose of TypeScript and can lead to unexpected errors.

Example (Not Recommended):

function doSomething(value: any): any {
  // No type safety here!
  return value.toUpperCase(); // Could cause runtime errors
}

What is the fundamental difference between Optional Chaining and Nullish Coalescing Operator in typescript?
Both optional chaining (?.) and the nullish coalescing operator (??) are powerful tools in TypeScript to handle potentially undefined or null values, but they serve different purposes and behave differently:

1. Purpose:

  • Optional Chaining (?.): Used to access properties or methods of an object safely when the object itself might be null or undefined. It prevents runtime errors by short-circuiting the expression if an intermediate value is null or undefined.
  • Nullish Coalescing (??): Used to provide a default value when a variable or expression evaluates to null or undefined. It doesn't care about falsy values like 0, '', or NaN - only strictly null or undefined.

2. Behavior:

  • Optional Chaining (?.): If any part of the chain encounters null or undefined, the entire expression evaluates to undefined without throwing an error.
  • Nullish Coalescing (??): Returns the first operand if it's not null or undefined. Otherwise, it returns the second operand (the default value).

Example:

Let's imagine we have a User object:

interface User {
  name: string;
  address?: {
    street: string;
    city: string;
  };
}

Here, the address property is optional (marked by ?).

Scenario 1: Accessing city safely:

const user: User = { name: 'Alice' }; 

// Without optional chaining:
// const city = user.address.city; // ERROR! Cannot read property 'city' of undefined

// With optional chaining:
const city = user.address?.city; 
console.log(city); // Output: undefined (no error thrown)

Scenario 2: Providing a default value for city:

const user1: User = { name: 'Bob', address: { street: 'Main St', city: 'New York' } };
const user2: User = { name: 'Charlie' }; 

const city1 = user1.address?.city ?? 'Unknown City';
const city2 = user2.address?.city ?? 'Unknown City';

console.log(city1); // Output: New York
console.log(city2); // Output: Unknown City

In summary:

  • Use ?. to safely navigate through potentially null/undefined properties or methods.
  • Use ?? to provide a fallback value specifically when encountering null or undefined.

What does short-circuiting mean in typescript?
Certainly, let's explore short-circuiting in TypeScript.

What is Short-Circuiting?

In essence, short-circuiting in TypeScript (and JavaScript, as they share this behavior) refers to the way logical operators && (AND) and || (OR) function within conditional expressions. When these operators are used, the evaluation of the expression might stop before examining all the operands if the result can be determined early on. Let me elaborate:

  • && (AND): If the left operand evaluates to false, the entire expression is guaranteed to be false. Consequently, the right operand is not evaluated.

  • || (OR): If the left operand evaluates to true, the entire expression is guaranteed to be true. Therefore, the right operand is skipped.

Why is it called Short-Circuiting?

Imagine an electrical circuit. A short circuit occurs when the current bypasses a portion of the intended path. Similarly, in programming, short-circuiting means the evaluation "bypasses" or skips the evaluation of certain expressions when the final outcome is already determined.

Example Time

Let's illustrate with a TypeScript code snippet:

function checkValue(value: number | null) {
  if (value !== null && value > 10) { 
    console.log("Value is greater than 10");
  } else {
    console.log("Value is null or not greater than 10");
  }
}

checkValue(12); // Output: Value is greater than 10
checkValue(null); // Output: Value is null or not greater than 10 

In this example:

  1. When checkValue(12) is called, value !== null is true, so the evaluation proceeds to value > 10, which is also true.

  2. However, when checkValue(null) is called, value !== null is false. Due to short-circuiting with &&, the expression value > 10 is never evaluated (which would cause an error if value were indeed null).

Benefits of Short-Circuiting

  • Improved Performance: By avoiding unnecessary computations, short-circuiting can make your code slightly faster, especially in complex conditions.

  • Concise Code: It enables you to write more compact and expressive conditional statements.

  • Conditional Execution: You can leverage short-circuiting to conditionally execute code, like calling a function only if a variable is not null.


List a few rules of private fields in typescript?

  1. Declaration with # Prefix:

    • Private fields are declared using a # prefix before the field name. This prefix signifies that the field is private and cannot be accessed outside the containing class.
  2. Accessibility Limited to Containing Class:

    • Unlike private in many other languages, TypeScript's # private fields are strictly bound to the class where they are declared. They cannot be accessed by subclasses or instances of the class from outside the class definition.
  3. No Access via this in Inner Scopes:

    • Within the class, even if you're inside a method or nested function, you cannot access private fields using this. when the context of this might change (e.g., in event handlers or callbacks).
  4. Runtime Enforcement:

    • TypeScript enforces private field access at compile time. This means that the compiler will prevent any code from accessing a private field from outside the class. However, the private fields are still present in the compiled JavaScript code (just with mangled names for uniqueness).
  5. No Static Private Fields (Yet):

    • Currently, TypeScript doesn't support static private fields. If you need a private static member, you can simulate it using a private instance field and access it through static methods within the class.

Example:

class BankAccount {
  #balance: number;

  constructor(initialBalance: number) {
    this.#balance = initialBalance;
  }

  deposit(amount: number): void {
    this.#balance += amount;
  }

  withdraw(amount: number): void {
    if (this.#balance >= amount) {
      this.#balance -= amount;
    } else {
      console.log("Insufficient funds.");
    }
  }

  // This method can access #balance because it's inside the class
  getBalance(): number {
    return this.#balance; 
  }
}

const myAccount = new BankAccount(1000);
myAccount.deposit(500);

// Error: Property '#balance' is private and only accessible within class 'BankAccount'.
// console.log(myAccount.#balance); 

console.log(myAccount.getBalance()); // Output: 1500 

What are some use cases of template literal types in typescript?
Template literal types in TypeScript provide a powerful mechanism to create new types based on the manipulation of existing string literal types. They use template literals (backticks ``) syntax to create these new types, offering flexibility similar to string manipulation but at the type level.

Here are some common use cases:

1. Dynamically Creating Type Aliases:

Template literal types are useful for dynamically creating type aliases based on other types.

Example:

type Fruit = "apple" | "banana";

// Create a type for each fruit with "Juice" suffix
type JuiceType<T extends string> = `${T}Juice`;

const appleJuice: JuiceType<"apple"> = "appleJuice"; // OK
const bananaSmoothie: JuiceType<"banana"> = "bananaSmoothie"; // OK

const invalidJuice: JuiceType<"grape"> = "grapeJuice"; // Error: "grapeJuice" is not assignable to "appleJuice" | "bananaJuice".

In this example, JuiceType is a generic type alias that takes a string literal type T. Using template literals, it creates a new type by appending "Juice" to the passed type. This allows us to define types like appleJuice and bananaJuice based on the Fruit type.

2. Type Validation for String Combinations:

Template literal types can enforce specific string patterns or combinations.

Example:

type Color = "red" | "green" | "blue";
type Size = "S" | "M" | "L";

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

const validId: ProductId = "blue-M"; // OK
const invalidId: ProductId = "blue-XL"; // Error: Type '"blue-XL"' is not assignable to type '"red-S" | "red-M" | "red-L" | "green-S" | "green-M" | "green-L" | "blue-S" | "blue-M" | "blue-L"'.

Here, ProductId combines Color and Size types using a hyphen. This ensures that only valid color-size combinations are allowed.

3. String Manipulation at the Type Level:

You can manipulate string literal types similarly to manipulating strings at runtime.

Example:

type Uppercase<T extends string> = `${Uppercase<T>}`;

const name: Uppercase<"john"> = "JOHN"; // OK
const wrongName: Uppercase<"john"> = "John"; // Error: Type '"John"' is not assignable to type '"JOHN"'.

This example defines a Uppercase type that converts the provided string literal type to uppercase.


How to check the type of variable or constant in typescript?
You don't directly check the type of a variable or constant at runtime in TypeScript. This is because TypeScript's type system is primarily a design-time feature. The type information is used by the compiler to check for errors and provide better tooling, but it is erased during the compilation to JavaScript.

However, you can leverage JavaScript's built-in operators and methods to infer the "type" of a value at runtime. Let me clarify what this means and provide examples:

Understanding TypeScript's Type System

TypeScript adds static typing to JavaScript. These types act as annotations for the compiler but don't exist at runtime:

let message: string = "Hello, TypeScript!"; 
message = 123; // Compiler error: Type 'number' is not assignable to type 'string'.

After compilation to JavaScript, the type annotation disappears:

let message = "Hello, TypeScript!";
message = 123; // This would run without issues in JavaScript.

Runtime Type Inspection in JavaScript (and TypeScript)

You can use JavaScript's mechanisms to get information about the value a variable holds at runtime:

  1. typeof operator: Useful for primitive types.

    let num = 10;
    let str = "Hello";
    let bool = true;
    let obj = {};
    let arr = [1, 2, 3];
    
    console.log(typeof num);   // "number"
    console.log(typeof str);   // "string"
    console.log(typeof bool);  // "boolean"
    console.log(typeof obj);   // "object"
    console.log(typeof arr);   // "object" (arrays are objects in JavaScript)
    console.log(typeof null);  // "object" (a quirk of JavaScript)
  2. instanceof operator: Checks if an object is an instance of a particular class.

    class Animal {}
    class Dog extends Animal {}
    
    let myDog = new Dog();
    
    console.log(myDog instanceof Dog);      // true
    console.log(myDog instanceof Animal);   // true (due to inheritance)
  3. Array.isArray(): Specifically for checking if a value is an array.

    let arr = [1, 2, 3];
    console.log(Array.isArray(arr));  // true
  4. Object Prototypes: You can inspect the prototype chain of an object to understand its "type" in a more traditional object-oriented sense.

    console.log(arr.__proto__ === Array.prototype); // true

How to add types to an interface from another interface or extend types in typescript?
In TypeScript, you can "extend" an interface by using the extends keyword. This allows you to copy the members of one interface into another, creating a new interface with the combined properties.

Example:

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

interface Employee extends BasicInfo {
  employeeId: number;
  department: string;
}

const employee: Employee = {
  name: "John Doe",
  age: 30,
  employeeId: 12345,
  department: "Engineering",
};

You can extend multiple interfaces by separating them with commas.

interface Address {
  street: string;
  city: string;
}

interface Contact {
  email: string;
  phone: string;
}

interface Customer extends BasicInfo, Address, Contact {
  customerId: number;
}

TypeScript allows you to extend types using intersections (&). This combines multiple types into one.

Example:

type Name = {
  firstName: string;
  lastName: string;
};

type Person = Name & {
  age: number;
};

const person: Person = {
  firstName: "Jane",
  lastName: "Doe",
  age: 25,
};

What are the differences between type and interface in typescript?
Both types and interfaces serve similar purposes in TypeScript: defining the structure and types of data. However, they differ in their capabilities and intended use cases.

Key Differences:

Feature Interface Type
Declaration Uses the interface keyword Uses the type keyword
Extension Can be extended using extends Can be extended using intersections (&)
Merging Can be merged with other interfaces Cannot be merged
Primitive Cannot be used to describe primitives Can describe primitives and complex types
Union/Tuple Cannot be used to create unions/tuples Can be used to create unions/tuples

1. Declaration:

  • Interface: Declared using the interface keyword.
  • Type: Declared using the type keyword.

2. Extension:

  • Interface: Can be extended using the extends keyword, inheriting properties from other interfaces.
  • Type: Can be combined and extended using intersection (&).

3. Merging:

  • Interface: Multiple declarations of the same interface name will merge their properties.
  • Type: Cannot be merged. Re-declaration will throw an error.

4. Describing Data:

  • Interface: Primarily used for describing the shape of objects. Cannot be used to describe primitives like string or number directly.
  • Type: Can describe a wider range of types, including primitives, unions, tuples, and more complex types.

5. Union and Tuple Types:

  • Interface: Cannot be used to directly define union or tuple types.
  • Type: Ideal for defining union and tuple types.

Example:

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

// Type
type Admin = User & {
  isAdmin: boolean;
};

// Interface Extension
interface Employee extends User {
  employeeId: number;
}

// Union Type
type Status = 'pending' | 'approved' | 'rejected';

// Tuple Type
type Point = [number, number];

In this example:

  • User is an interface defining the structure of a user object.
  • Admin is a type extending the User interface and adding an isAdmin property.
  • Employee is an interface extending the User interface and adding an employeeId property.
  • Status is a union type defining possible statuses.
  • Point is a tuple type defining a point with two numbers.

When to Use Which?

  • Interface: Use interfaces for defining the shape of objects, especially when you need to:

    • Use object-oriented concepts like inheritance.
    • Allow for open-ended extension and merging of definitions.
  • Type: Use types for:

    • Defining primitive types, unions, tuples.
    • Creating aliases for complex types.
    • Situations where you don't need the merging behavior of interfaces.

Are strongly-typed functions as parameters possible in typescript?
Let's break down strongly-typed function parameters in TypeScript and illustrate with an example.

Strongly-Typed Function Parameters: The Essence

In TypeScript, "strongly-typed function parameters" means you can explicitly define the data type that a function expects for each of its arguments. This offers several advantages:

  • Clarity and Maintainability: Your code becomes self-documenting. Anyone looking at the function signature immediately understands what kind of data it requires.
  • Early Error Detection: TypeScript's type checker identifies mismatched argument types during development, preventing runtime surprises.
  • Code Completion and Tooling: Your editor can provide intelligent suggestions and auto-completion, streamlining your development process.

Example: Calculating Area

function calculateArea(width: number, height: number): number {
  return width * height;
}

const area1 = calculateArea(10, 5); // Correct usage
console.log(area1); // Output: 50

const area2 = calculateArea("10", 5);  // Type error!
// Error: Argument of type 'string' is not assignable to parameter of type 'number'.

Explanation:

  1. Function Signature:

    • calculateArea(width: number, height: number): number
      • width: number: The width parameter must be a number.
      • height: number: The height parameter must be a number.
      • : number: The function's return type is a number.
  2. Correct Usage: calculateArea(10, 5) passes two numbers as arguments, satisfying the type requirements.

  3. Type Error: calculateArea("10", 5) attempts to pass a string ("10") for the width parameter, triggering a type error during compilation.

Key Points:

  • Type Inference: In many cases, TypeScript can infer the types of your function parameters if you don't explicitly specify them. However, it's considered good practice to include type annotations for better readability and type safety.
  • Flexibility: TypeScript allows you to use union types (number | string), optional parameters (e.g., width?: number), and more to create versatile function signatures.

How to make an array with a specific length or array elements in typescript?
There are several ways to create arrays with predefined lengths or elements in TypeScript:

1. Array Constructor with length Property:

  • You can use the Array constructor with the length property to create an empty array with a specific length.
const myArray: number[] = new Array(5); // Creates an array with length 5

console.log(myArray); // Output: [empty × 5] 

This creates an array with 5 empty slots. Note that these slots are not actually initialized with any values. You will need to iterate over the array and assign values to each element if you want to use them.

2. Array.from() with map():

  • This approach is useful when you need to populate the array with initial values during creation.
  • Array.from() creates a new array from an array-like or iterable object.
  • You can provide an object with a length property and then use map() to assign values to each element.
const myArray: number[] = Array.from({ length: 5 }, (_, i) => i + 1); // [1, 2, 3, 4, 5]

console.log(myArray); // Output: [1, 2, 3, 4, 5]

This code creates an array of length 5, where each element is assigned a value based on its index plus 1.

3. Array Literal with Specific Elements:

  • This is the most straightforward method for creating arrays with known elements.
  • You simply define the array and list the desired elements within square brackets [].
const myArray: string[] = ["apple", "banana", "cherry"]; 

console.log(myArray); // Output: ["apple", "banana", "cherry"]

4. Array.fill():

  • This method is useful when you want to create an array with all elements initialized to the same value.
const myArray: boolean[] = new Array(3).fill(true); // [true, true, true]

console.log(myArray); // Output: [true, true, true]

This code creates an array of length 3 and fills it with the boolean value true.

Choosing the Right Method:

  • If you need an empty array with a specific length, use the Array constructor with the length property.
  • If you need to populate the array with initial values based on their index, use Array.from() with map().
  • If you know the elements in advance, use an array literal.
  • If you need an array filled with the same value, use Array.fill().

By understanding these different methods, you can choose the most efficient and readable way to create arrays in your TypeScript code.


Is there a way to check for both null and undefined in typescript?

There are multiple ways to efficiently check for both null and undefined in TypeScript:

1. Strict Equality (===) and Logical OR (||):

This method directly compares the variable with null and undefined using the strict equality operator (===) and combines the checks with a logical OR (||).

function isNullOrUndefined(value: any): boolean {
  return value === null || value === undefined;
}

let myVariable: string | null | undefined;

if (isNullOrUndefined(myVariable)) {
  console.log("The variable is null or undefined.");
} else {
  console.log("The variable is:", myVariable); 
}

2. Loose Equality (==) with Null:

This method leverages the fact that in JavaScript (and therefore TypeScript), null == undefined returns true. Therefore, comparing the variable with null using loose equality (==) effectively checks for both null and undefined.

function isNullOrUndefined(value: any): boolean {
  return value == null; 
}

let myVariable: number | null | undefined;

if (myVariable == null) { 
  console.log("The variable is null or undefined.");
} else {
  console.log("The variable is:", myVariable);
}

3. Optional Chaining (?.) with Nullish Coalescing Operator (??):

This approach is particularly useful when accessing properties of potentially null or undefined objects. The optional chaining operator (?.) will short-circuit the property access if the object is null or undefined, preventing errors. The nullish coalescing operator (??) provides a fallback value if the preceding expression is null or undefined.

interface User {
  name: string;
}

let user: User | null | undefined;

// Safe access with fallback
const username = user?.name ?? "Guest";
console.log("Username:", username);

Explanation:

  • Strict Equality (===): This operator compares both the value and the type without any type coercion. It's the safest and most explicit way to check for null or undefined.
  • Loose Equality (==): This operator allows for type coercion. While it can be convenient for checking null and undefined together, it might lead to unexpected behavior in other comparisons.
  • Optional Chaining (?.): This operator allows safe property access on potentially null or undefined objects, avoiding runtime errors.
  • Nullish Coalescing Operator (??): This operator provides a concise way to handle default values when dealing with null or undefined.

Choosing the right method depends on your specific use case and coding style. Using strict equality and explicitly checking for both null and undefined is generally recommended for clarity and type safety. However, loose equality with null can be a concise option when you specifically need to check for both values. Optional chaining and the nullish coalescing operator provide elegant solutions for safe property access and default value handling.


How does override keyword work in typescript?
Certainly, let's break down how the override keyword functions in TypeScript.

Purpose of override

In essence, the override keyword in TypeScript serves as a safety net and a tool for clarity when dealing with inheritance and method overriding. Its primary roles are:

  1. Explicit Override: It explicitly signals that a method in a subclass is intended to override a method from its superclass. This enhances code readability by making it immediately clear that you're modifying inherited behavior.

  2. Error Prevention: Crucially, override helps prevent accidental errors. If you use override and the method signature doesn't actually match a method in the superclass (or any ancestor class), TypeScript will raise a compile-time error. This safeguards you from situations where you might have misspelled a method name or unintentionally changed parameters, leading to unexpected runtime issues.

Illustrative Example

class Shape {
  area(): number {
    return 0; // Base implementation
  }
}

class Circle extends Shape {
  radius: number;

  constructor(radius: number) {
    super();
    this.radius = radius;
  }

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

const myCircle = new Circle(5);
console.log(myCircle.area()); // Output: 78.53981633974483

What is Mixin Constructor Type in typescript?
Let's break down mixin constructors in TypeScript.

Understanding Mixins

In essence, mixins offer a way to assemble classes by combining functionalities from different sources, kind of like adding ingredients to a recipe. They provide flexibility, particularly when you want to share behaviors across unrelated classes without relying heavily on traditional inheritance.

Mixin Constructor Types: The Blueprint

Now, imagine you want to create a mixin that introduces new properties or methods to the classes it enhances. This is where "mixin constructor types" come into play. They define the structure (types of parameters, return type) of the constructor function used to create your mixin.

Key Points

  • Generic Type Parameters: Mixin constructor types often employ generic type parameters (<T> in the examples below) to work with a variety of base classes without knowing their specific types beforehand.
  • Extending Constructors: They typically extend the constructor of the base class to ensure the mixin's constructor can properly initialize instances of the combined class.

Example: Applying a Timestamp Mixin

Let's say we want to add a timestamp functionality (recording creation time) to different classes.

// 1. Define the Mixin Constructor Type
type TimestampMixinConstructor<T extends new (...args: any[]) => any> = 
  new (...args: ConstructorParameters<T>) => T & { createdAt: Date };

// 2. Create the Mixin Function
function withTimestamp<T extends new (...args: any[]) => any>(
  BaseClass: T
): TimestampMixinConstructor<T> {
  return class extends BaseClass {
    createdAt = new Date();
  };
}

// 3. Usage Example
class User {
  constructor(public name: string) {}
}

const TimeStampedUser = withTimestamp(User); 
const user = new TimeStampedUser("Alice");
console.log(user.name);     // Output: "Alice"
console.log(user.createdAt); // Output: Current timestamp

Explanation:

  1. TimestampMixinConstructor: This type alias defines the structure of our mixin's constructor. It takes a generic type T (representing the base class's constructor) and specifies that the resulting constructor:

    • Accepts the same parameters as the base class's constructor (ConstructorParameters<T>).
    • Returns an object that's both an instance of the base class (T) and has an added createdAt property of type Date.
  2. withTimestamp: Our mixin function.

    • It takes a class (BaseClass) as input.
    • It returns a new class that extends BaseClass.
    • The extended class adds the createdAt property and initializes it to the current date.
  3. Usage: We use withTimestamp to enhance the User class, creating a new TimeStampedUser class. Instances of TimeStampedUser now have both the name property from the original User class and the added createdAt property.


What is dynamic import expression in typescript?
Dynamic import expressions offer a powerful way to load JavaScript modules on demand at runtime. This contrasts with static imports, which load modules at compile time and can impact initial loading performance.

Here's a breakdown of dynamic imports in TypeScript:

Syntax:

import(<moduleSpecifier>).then((module) => {
    // Use the imported module here
});
  • import(<moduleSpecifier>): This part initiates the import process. It takes the module path as a string (similar to require() in Node.js).
  • .then((module) => ...): Since dynamic imports are asynchronous, they return a Promise. We use .then() to handle the resolved module.
  • module: The module object within the .then() block provides access to the imported module's exported members (functions, classes, variables, etc.).

Benefits of Dynamic Imports:

  • Improved Performance (Lazy Loading): Load modules only when needed, enhancing initial load times.
  • Code Splitting: Split your codebase into smaller chunks that can be loaded on demand, improving organization and maintainability.
  • Conditional Loading: Load modules based on specific conditions or user interactions.

Example:

Imagine you have a module mathUtils.ts with a function to calculate the factorial:

// mathUtils.ts
export function factorial(n: number): number {
  if (n === 0) {
    return 1;
  }
  return n * factorial(n - 1);
}

Now, you can dynamically import and use this function in your main application file:

// main.ts
const button = document.getElementById('calculateBtn') as HTMLButtonElement;

button.addEventListener('click', async () => {
  const { factorial } = await import('./mathUtils'); // Dynamic import
  const result = factorial(5); 
  console.log(`Factorial of 5 is: ${result}`); 
});

Explanation:

  1. We use async/await to handle the Promise returned by the dynamic import.
  2. The factorial function becomes available only after the mathUtils.ts module is loaded successfully.
  3. This approach ensures that the mathUtils module is loaded only when the button is clicked, optimizing performance.

Key Points:

  • Dynamic imports work with both JavaScript modules (ES modules) and TypeScript namespaces.
  • TypeScript's type system understands dynamic imports, providing type checking for the imported module's members.

Explain what is never datatype in typescript?
Let's break down the "never" datatype in TypeScript:

What is "never"?

In TypeScript, the never type represents values that should never occur. It's like saying "this code path should be logically impossible to reach."

Here's how it's different from other types:

  • never vs. void: While void indicates the absence of a return value (a function might return nothing, or undefined), never means a function will never return at all.

  • never vs. undefined or null: undefined and null are values that can exist in your code, potentially indicating the absence of data. never signifies that a value cannot logically exist.

Example Scenarios

  1. Functions Throwing Errors:

    function alwaysThrowsError(): never {
        throw new Error("This function always throws!");
    }
    
    // Attempting to use the return value will result in a type error
    let result: string = alwaysThrowsError(); 

    Here, alwaysThrowsError is typed as never because it always throws an error, preventing it from ever reaching a normal return statement.

  2. Infinite Loops:

    function infiniteLoop(): never {
        while (true) {
            // Do something forever...
        }
    }

    infiniteLoop is also typed as never because, if it functions correctly, it will never finish executing and thus never return a value.

Why is never Useful?

  • Improved Type Safety: The never type helps TypeScript's type system reason more effectively about your code's behavior. It can catch potential errors by flagging situations where you might be expecting a value but will never receive one.

  • Code Clarity: Using never makes your code more self-documenting. It clearly communicates to other developers (and your future self) that certain code paths are not intended to be reached.

Key Points to Remember

  • A function returning never must not have any reachable return statements (including implicit returns).
  • A function with an infinite loop or an unconditional throw statement can be inferred to have a return type of never.

Explain what is currying in typescript?
Currying is a functional programming technique in which a function that takes multiple arguments is transformed into a sequence of nested functions that each take a single argument.

In simpler terms:

Instead of having one function that takes all the arguments at once, you break it down into multiple functions, where each function takes one argument and returns another function that takes the next argument, and so on. The last function in the chain returns the final result.

Why use Currying?

  • Improved Code Reusability: You can create specialized functions by pre-filling some arguments.
  • Enhanced Code Readability: Functions become more concise and focused on a single task.
  • Deferred Execution: Arguments can be supplied one at a time, allowing computation to be delayed until all inputs are available.

Example:

Let's say we want to create a function to calculate the volume of a box:

Without Currying:

function volume(length: number, width: number, height: number): number {
  return length * width * height;
}

const boxVolume = volume(5, 4, 3); 
console.log(boxVolume); // Output: 60

With Currying:

function curriedVolume(length: number): (width: number) => (height: number) => number {
  return (width: number) => (height: number) => length * width * height;
}

const calculateVolume = curriedVolume(5);
const volumeWithWidth = calculateVolume(4);
const boxVolume = volumeWithWidth(3); 
console.log(boxVolume); // Output: 60

// Or, we can call it in one line:
const oneLineVolume = curriedVolume(5)(4)(3);
console.log(oneLineVolume); // Output: 60

Explanation:

  1. curriedVolume: This function takes the length argument and returns a new function.
  2. Returned Function (1st level): This function takes the width argument and returns another function.
  3. Returned Function (2nd level): This final function takes the height argument and calculates the volume using all three arguments (length, width, height).

Benefits:

  • We can reuse the calculateVolume function for different widths and heights while keeping the length constant.
  • Each function call is more focused, making the code easier to understand.

Why is the infer keyword needed in typescript?
Let's break down TypeScript's infer keyword and illustrate its usefulness.

The "Deduction" Challenge

TypeScript shines at catching type errors before your code even runs. However, there are times when TypeScript needs a little help figuring out the exact types involved, especially when dealing with generics. Consider this scenario:

function getFirstElement<T>(arr: T[]): T { 
    return arr[0];
}

const numbers = [1, 2, 3];
const firstNumber = getFirstElement(numbers); 

Here, TypeScript uses the generic type parameter T to make getFirstElement flexible, working with arrays of any type. While TypeScript infers firstNumber to be of type number, there are situations where the type inference might not be so straightforward.

infer: The Type Detective

The infer keyword acts like a powerful type detective within TypeScript's type system. It allows you to extract and capture the type of a value inside a generic type. Think of it as a way to say, "Hey TypeScript, figure out this type for me and let me use it."

Example: Deconstructing Function Return Types

Let's say you have a function that returns a Promise:

async function fetchData(): Promise<string> {
    // ... some asynchronous operation that returns a string
}

Now, imagine you want to create a generic type that extracts the type of the resolved value from any Promise-returning function:

type PromiseReturnType<T extends (...args: any[]) => Promise<infer R>> = R; 

Let's break this down:

  1. PromiseReturnType<T>: This is our generic type. It takes another type T as an argument.
  2. T extends (...args: any[]) => Promise<infer R>: This is a constraint. It ensures that the type T passed to PromiseReturnType is a function that:
    • Accepts any number of arguments (...args: any[]).
    • Returns a Promise.
  3. infer R: This is the magic! infer R tells TypeScript to infer the type of the value the Promise resolves to and temporarily name it R.
  4. = R: Finally, we tell TypeScript that the PromiseReturnType itself should be the inferred type R.

Putting it to Use:

type DataType = PromiseReturnType<typeof fetchData>; // DataType is now `string`

Key Takeaways

  • Type Inference Enhancement: infer helps TypeScript deduce types in complex scenarios where it might otherwise struggle.
  • Generic Flexibility: It shines when working with generics, allowing you to write more reusable and type-safe code.
  • Common Use Cases: You'll frequently encounter infer when working with functions (extracting return types), Promises, arrays, and more complex data structures.

What is the difference between unknown and any type in typescript?
Let's break down the differences between unknown and any in TypeScript, along with illustrative examples:

1. any

  • Meaning: The any type essentially bypasses TypeScript's type system. It tells TypeScript, "Trust me, I know what I'm doing." When a variable has the any type, you can assign it any value, and TypeScript won't perform type checking on it.

  • Flexibility: Extreme. You lose all type safety benefits with any.

  • Use Cases:

    • Migrating Existing JavaScript: Helpful when you're gradually introducing TypeScript to a large JavaScript codebase.
    • Interacting with Dynamic Libraries: When working with libraries that rely heavily on runtime type manipulation.
    • (Avoid if Possible) When You Don't Know the Type (Yet): While tempting, it's generally better to use unknown instead (explained below).

Example:

let value: any = 'Hello, world!'; 
value = 42; // No error 
value = { name: 'Alice' }; // No error 
value.doSomething(); // No error at compile time, but potential runtime error

2. unknown

  • Meaning: The unknown type represents a value whose type is uncertain. It's like saying, "I don't know what type this is yet, so I need to be careful." TypeScript enforces strict type checking when dealing with unknown.

  • Safety: Encourages safer code by making you prove the type before using it.

  • Use Cases:

    • Data from External Sources: Data fetched from an API, user input, etc.
    • Functions with Unpredictable Return Types: When you can't be certain what a third-party function might return.
    • Enforcing Type Safety: To prevent accidental misuse of variables with uncertain types.

Example:

let userInput: unknown;

userInput = 'Hello';
userInput = 123; 
userInput = { message: 'Welcome' };

// Error: Object is of type 'unknown'
// console.log(userInput.message); 

// Type Assertion (Be Careful!)
if (typeof userInput === 'object' && userInput !== null && 'message' in userInput) {
    console.log(userInput.message); 
}

// Type Narrowing (Safer)
if (typeof userInput === 'string') {
    console.log(userInput.toUpperCase()); 
}

Key Takeaways

  • any disables type checking entirely. It offers maximum flexibility but sacrifices type safety.
  • unknown represents a value with an unknown type. It requires type checking before you can use it, promoting safer code.

General Rule: Prefer unknown over any whenever possible. Only resort to any in situations where type information is genuinely unavailable or you're intentionally bypassing type safety during a transition phase.


How to exclude a property from type in typescript?
TypeScript offers several approaches to exclude properties from existing types, allowing you to create new types with specific attributes removed. Let's explore two common methods:

1. Using the Omit Utility Type:

The Omit<Type, Keys> utility type is a convenient way to construct a new type by picking all properties from Type except those specified in Keys.

Example:

interface User {
  id: number;
  name: string;
  email: string;
  createdAt: Date;
}

// Exclude 'email' and 'createdAt' properties
type PublicUser = Omit<User, 'email' | 'createdAt'>;

const user: User = {
  id: 1,
  name: "Alice",
  email: "[email protected]",
  createdAt: new Date(),
};

const publicUser: PublicUser = {
  id: user.id,
  name: user.name,
}; // Valid, as 'email' and 'createdAt' are excluded

In this example, PublicUser will have the same properties as User except for email and createdAt.

2. Using Custom Type Definitions with Pick and Exclude:

You can manually define a new type by selectively including or excluding properties using Pick and Exclude utility types.

Example:

interface User {
  id: number;
  name: string;
  email: string;
  createdAt: Date;
}

// Define keys to keep
type UserKeys = 'id' | 'name';

// Exclude 'email' and 'createdAt' using Exclude
type ExcludedKeys = Exclude<keyof User, UserKeys>; 

// Create a new type with excluded keys omitted
type PublicUser = Pick<User, Exclude<keyof User, ExcludedKeys>>;

const publicUser: PublicUser = {
  id: 1,
  name: "Bob",
}; // Valid, as only 'id' and 'name' are included

Here, we explicitly define the keys to keep (UserKeys) and use Exclude to get the keys to omit (ExcludedKeys). Then, we use Pick to construct the PublicUser type with desired properties.

Both approaches effectively achieve property exclusion. Choose the method that aligns best with your code style and complexity.


Why we need Index Signature in typescript?
Let's dive into index signatures in TypeScript:

Why Index Signatures?

Index signatures in TypeScript solve a fundamental problem when working with objects where you don't know the exact property names beforehand. Here's the breakdown:

  • Flexibility with Dynamic Objects: Imagine fetching data from an API, where the structure of the response might vary. Or, you might be building a system where users can define custom properties on objects. Index signatures provide a way to tell TypeScript, "Hey, this object can have properties I don't explicitly list in advance, but here's how they should be typed."

  • Type Safety for Dynamic Data: Without index signatures, you'd have to rely on type assertions (as) or any, sacrificing type safety. Index signatures maintain type checking even when you're dealing with less predictable object structures.

Example: A Simple Shopping Cart

Let's say you're building a shopping cart. You know each item will have a name (string) and a price (number), but you don't want to hardcode every possible item in your code.

interface ShoppingCartItem {
  name: string;
  price: number;
}

// This works, but it's not very flexible
let cart: { [itemName: string]: ShoppingCartItem } = {}; 

cart.apple = { name: "Apple", price: 0.99 };
cart.banana = { name: "Banana", price: 0.79 };

// Now you can access items dynamically
console.log(cart.apple.price); // 0.99

Explanation

  1. interface ShoppingCartItem: This defines the structure of each item in your cart.

  2. [itemName: string]: ShoppingCartItem: This is the index signature. Let's break it down:

    • [itemName: string]: This indicates that the keys of the cart object will be strings (itemName is just a descriptive name for the key).
    • ShoppingCartItem: This specifies that the value associated with each key (itemName) must conform to the ShoppingCartItem interface.
  3. Flexibility: You can now add any number of items to your cart dynamically, and TypeScript will ensure that each item has the correct properties (name and price).

Key Points

  • Index signatures describe the types of keys and values allowed in an object.
  • They provide type safety for situations where you work with objects that have a dynamic or unpredictable structure.
  • Index signatures are invaluable when dealing with data from APIs, user-generated content, or any scenario where flexibility in object properties is required.

How to define a typescript class which has an index signature?
Certainly, let's explore how to define TypeScript classes with index signatures.

Understanding Index Signatures

In TypeScript, an index signature allows you to define the type of properties an object can have without specifying each property name explicitly. This is particularly useful when you're dealing with objects where the property names are dynamic or unknown at compile time.

Defining a Class with an Index Signature

To incorporate an index signature within a TypeScript class, you declare it as part of the class's properties. The syntax resembles that of an object literal's index signature.

Example: Dynamic Data Store

Let's imagine you're creating a class to represent a simple data store that can hold key-value pairs of data:

class DataStore<T> {
  // Index signature for dynamic properties
  [key: string]: T; 

  // Method to add data
  addData(key: string, value: T): void {
    this[key] = value;
  }

  // Method to retrieve data
  getData(key: string): T {
    return this[key];
  }
}

Explanation

  1. [key: string]: T: This is the index signature. It states that our DataStore class can have properties with keys of type string and values of type T (a generic type parameter, making our class flexible to store different data types).

  2. addData and getData: These methods demonstrate how to use the index signature. We can directly access properties using bracket notation (e.g., this[key]) as if they were pre-defined properties of the class.

Usage

const myStore = new DataStore<number>();
myStore.addData("apples", 10);
myStore.addData("bananas", 5);

console.log(myStore.getData("apples")); // Output: 10
console.log(myStore["bananas"]); // Also valid: Output: 5

Caveats

  • You can have only one index signature per object type (class or interface).
  • The index signature only permits reading and writing properties with the specified key type (in our example, string). Trying to access properties with other key types will result in a type error.

In Essence

Index signatures in TypeScript classes provide a powerful way to handle dynamic objects and create more flexible data structures. They allow you to work with properties that aren't explicitly defined in the class declaration, enhancing the expressiveness of your code.


What is one thing you would change about typescript?
One thing I would change about TypeScript is the way it handles optional chaining and function calls. Currently, if you use optional chaining to access a potentially undefined function and then try to call it, TypeScript throws an error even if you've checked for its existence.

Current Behavior:

interface User {
  name: string;
  getDiscount?: () => number;
}

const user: User = { name: "Alice" };

// Error: Property 'getDiscount' does not exist on type 'User | undefined'.
const discount = user?.getDiscount()?.(); 

Desired Behavior:

I believe TypeScript should be smart enough to infer that since we're using optional chaining twice, the final function call is only executed if getDiscount exists and returns a function. The code above should ideally work without errors.

Explanation:

The current behavior forces developers to use verbose null/undefined checks or type assertions, even when the optional chaining syntax already expresses the intent clearly. This hinders readability and can make the code unnecessarily complex.

Example of Improvement:

With the proposed change, the code would be much cleaner and easier to understand:

// No error, discount will be undefined if getDiscount is undefined or doesn't return a function
const discount = user?.getDiscount()?.(); 

if (discount) {
  // Apply discount
} else {
  // Handle case where discount is not available
}

This improvement would make TypeScript more intuitive and less verbose, allowing developers to focus on the logic of their code instead of working around type system limitations.


Explain when to use declare keyword in typescript?
Let's break down the "declare" keyword in TypeScript:

The Purpose of declare

In essence, the declare keyword tells TypeScript, "Trust me, this exists somewhere else." It's used to signal that you're working with external or ambient declarations – code that's not directly part of your current TypeScript file but will be present at runtime.

When to Use declare

  1. Integrating with JavaScript Libraries: Many JavaScript libraries don't come with built-in TypeScript type definitions. The declare keyword lets you create these definitions, bridging the gap and enabling type checking when you use these libraries.

    // Example: Defining types for a hypothetical 'myLibrary' 
    declare function greet(name: string): void;
    declare const myLibrary: {
        version: string;
        utils: {
            capitalize: (text: string) => string;
        }
    };
    
    greet("Alice"); // Type-safe usage
    console.log(myLibrary.version);
  2. Working with Global Variables: If your code interacts with global variables defined elsewhere (e.g., in a script tag or another module), use declare to tell TypeScript about them.

    // Assume a global variable 'globalCounter' exists
    declare var globalCounter: number; 
    
    globalCounter++; // Type-safe access
  3. Defining Module Boundaries: When creating your own libraries or modules, you might want to expose specific types to the outside world. declare helps you explicitly mark these types.

    // In a file like 'myModule.d.ts'
    declare class Calculator {
        add(a: number, b: number): number;
    }

Important Points

  • No Implementation: declare doesn't generate any JavaScript code. It's purely for TypeScript's type checking system.
  • Placement: Declarations are often placed in separate files with the extension .d.ts (TypeScript definition files). This keeps your type information organized.

Example: Typings for a JavaScript Library

Let's say you're using a simple JavaScript library that provides date formatting:

// date-formatter.js
function formatDate(date, format) {
  // ... Implementation for formatting
}

You'd create a TypeScript definition file (date-formatter.d.ts):

// date-formatter.d.ts
declare function formatDate(date: Date, format: string): string;

Now TypeScript can understand how to use formatDate correctly.

In summary, declare is your tool for bringing external JavaScript code into the world of TypeScript type safety.


Is it possible to generate typescript declaration file from javascript library?
Yes, it's possible to generate TypeScript declaration files (.d.ts) from JavaScript libraries. This allows you to use JavaScript libraries in your TypeScript projects with type safety and code completion.

Here's why and how you can do it:

Why Generate Declaration Files?

  • Type Safety: TypeScript can't inherently understand the types within a JavaScript library. Declaration files provide type information for these libraries, enabling type checking and preventing potential runtime errors.
  • Code Completion: Declaration files allow IDEs to offer intelligent code completion and suggestions when you're using the JavaScript library within your TypeScript code.
  • Improved Developer Experience: Having type information makes your code more readable and easier to maintain, especially when working with large codebases and teams.

Methods for Generating Declaration Files:

  1. Using Bundled Declaration Files:

    Many popular JavaScript libraries already include declaration files. These are often published alongside the library code. You can install them directly from npm or yarn.

    npm install @types/library-name
  2. Using a Declaration File Generator:

    Several tools can automatically generate declaration files from your JavaScript code. Here are some popular options:

    • dts-gen: A simple command-line tool that analyzes your JavaScript code and generates basic declaration files. (https://github.com/microsoft/dts-gen)

      npm install -g dts-gen
      dts-gen -s path/to/your/library.js -o path/to/your/library.d.ts
    • Typedoc: A documentation generator that can also generate declaration files as part of its output. (https://typedoc.org/)

      npm install -g typedoc
      typedoc --out docs/ --declaration path/to/your/library.js
  3. Manually Writing Declaration Files:

    For smaller libraries or specific cases, you can write declaration files manually. This provides the most control but can be time-consuming.

Example:

Let's say you have a simple JavaScript library in math.js:

// math.js
function add(a, b) {
  return a + b;
}

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

module.exports = { add, subtract };

You can use dts-gen to generate a declaration file:

dts-gen -s math.js -o math.d.ts 

This will create math.d.ts:

// math.d.ts
declare function add(a: number, b: number): number;
declare function subtract(a: number, b: number): number;
declare const _default: {
    add: typeof add;
    subtract: typeof subtract;
};
export default _default;

Now you can use the library in your TypeScript code with type checking:

// main.ts
import * as math from './math';

const result = math.add(5, 3); 
console.log(result); // Output: 8

Important Considerations:

  • JSDoc Comments: Using JSDoc comments in your JavaScript code can significantly improve the accuracy and completeness of automatically generated declaration files.
  • Accuracy: Automatically generated declaration files may not always be perfect and might require manual adjustments or refinements.
  • Maintenance: If your JavaScript library changes frequently, you'll need to regenerate or update the declaration files to maintain type safety.

What are Ambients in typescript and when to use them?
Let's break down Ambient Declarations in TypeScript:

What are Ambient Declarations?

In essence, Ambient Declarations in TypeScript act as blueprints or declarations for external entities like existing JavaScript code, libraries, or variables that exist outside your TypeScript codebase. They tell the TypeScript compiler, "Hey, trust me, this thing exists and has this specific shape, even though you can't see its implementation right now."

Key Points:

  • Declaration, not Implementation: Ambients declare the structure (types) but don't provide the actual code. Think of them like interfaces for external elements.
  • Bridging the Gap: They bridge the gap between your TypeScript code and the JavaScript world, enabling type safety even when interacting with external libraries.
  • No Code Emission: TypeScript doesn't generate any JavaScript code from ambient declarations. They exist solely for type checking during compilation.

When to Use Ambient Declarations:

  1. Integrating Existing JavaScript Libraries: If you're using a JavaScript library without built-in TypeScript type definitions, you'll use ambient declarations to describe its types so TypeScript can understand how to work with it.

  2. Global Variables: When you have global variables (like those attached to the window object in a browser) that TypeScript isn't aware of, you can declare them with ambient declarations to avoid errors.

  3. Declaring Modules: If you're creating your own modules without using TypeScript's module system (e.g., using global variables), ambient declarations help define the module's structure.

Example: Integrating jQuery (Hypothetical)

Let's imagine jQuery didn't have built-in type definitions. You'd create an ambient declaration like this (usually in a .d.ts file):

// jquery.d.ts

declare var $: JQueryStatic; // Declaring the global '$'

interface JQueryStatic {
    (selector: string): JQuery; // A function that returns a JQuery object
    ajax(settings: JQueryAjaxSettings): JQueryXHR; // An example method
    // ... more method declarations
}

interface JQuery {
    // ... methods available on a JQuery object
    text(newText?: string): string; 
    on(events: string, handler: (eventObject: JQueryEventObject) => any): this;
    // ... and so on
}

interface JQueryAjaxSettings {
    url: string;
    method?: string;
    // ... other settings
}

// ... more interfaces for JQuery types (e.g., JQueryXHR, JQueryEventObject)

Explanation:

  • declare var $: This line declares a global variable named $ and tells TypeScript it's of type JQueryStatic.
  • interface JQueryStatic ...: This interface defines the shape of the JQueryStatic object (what $ points to), including methods like ajax.
  • interface JQuery ...: This interface describes the methods available on a JQuery object (the result of $(...)).
  • The .d.ts Extension: It's a convention to put ambient declarations in files ending with .d.ts (declaration files).

How It Works:

When you include the jquery.d.ts file in your project (often using a tsconfig.json setting), TypeScript can now understand how to use jQuery, providing type checking and autocompletion even though jQuery itself is written in plain JavaScript.

Important Notes:

  • Type Definitions for Popular Libraries: Most popular JavaScript libraries have well-maintained type definitions available on DefinitelyTyped (https://definitelytyped.org/). You install them as npm packages (usually prefixed with @types/).

  • Accuracy Matters: The accuracy of your ambient declarations is crucial. Incorrect type definitions can lead to unexpected runtime errors even if TypeScript compiles your code.


Explain the difference between declare enum and declare const enum in typescript?
Let's break down the differences between declare enum and declare const enum in TypeScript:

1. declare enum

  • Purpose: Tells TypeScript that an enum exists elsewhere, usually in a separate JavaScript file. It doesn't generate any JavaScript code itself.
  • When to use: Primarily for type checking and code completion when working with existing JavaScript codebases that use enums.

Example:

// my-enums.js (or a separate .ts file that gets compiled to .js)
const MyEnum = {
    Value1: 0,
    Value2: 1
};

// my-typescript-file.ts
declare enum MyEnum {
    Value1,
    Value2
}

let myValue: MyEnum = MyEnum.Value1; 
console.log(myValue); // This will log 0

Explanation:

  • In my-enums.js, a standard JavaScript object mimics an enum.
  • In my-typescript-file.ts, declare enum informs TypeScript about the structure of MyEnum without generating any JavaScript output for it. This lets you use MyEnum for type safety, but at runtime, you're working directly with the JavaScript object from my-enums.js.

2. declare const enum

  • Purpose: Similar to declare enum, it signals the existence of an enum defined elsewhere. The crucial difference is that it instructs the TypeScript compiler to perform inlining.
  • When to use: When you want to combine type safety with the performance benefits of direct value substitution in your JavaScript output.

Example:

// my-enums.js (or a separate .ts file compiled to .js)
const MyConstEnum = {
    ValueA: "a",
    ValueB: "b"
};

// my-typescript-file.ts
declare const enum MyConstEnum {
    ValueA = "a",
    ValueB = "b"
}

function logValue(val: MyConstEnum) {
    console.log(val); 
}

logValue(MyConstEnum.ValueA); // After compilation: console.log("a");

Explanation:

  • In my-enums.js, you have your JavaScript enum-like object.
  • In my-typescript-file.ts, declare const enum tells TypeScript about MyConstEnum.
  • During compilation, TypeScript replaces MyConstEnum.ValueA directly with its string literal value "a" in the generated JavaScript code. This eliminates the runtime lookup overhead you'd have with a regular declare enum.

Key Takeaways

  • declare enum: For type information and compatibility with existing JavaScript enums; no code generation.
  • declare const enum: Inlines values at compile time for improved runtime performance; requires the enum to be defined elsewhere.

Important Note: While declare const enum enhances performance, be mindful that the values need to be known at compile time. If your enums are dynamically generated or loaded from external sources, you'll have to rely on declare enum.


How the never datatype can be useful in typescript?
The never type in TypeScript represents values that never occur.

Here's why it's useful:

  1. Function Exhaustiveness Checking:

    • When a function is annotated to return never, TypeScript enforces that it should never reach its end point. This helps catch potential logic errors.
    function handleShape(shape: string): never {
      switch (shape) {
        case 'circle':
          // ... handle circle
          return; 
        case 'square':
          // ... handle square
          return; 
        default:
          const exhaustiveCheck: never = shape; // Error if shape could be another value
          throw new Error(`Unknown shape: ${shape}`);
      }
    }

    In this example, if you add a new shape type later but forget to update the handleShape function, TypeScript will raise an error because the default case would now be reachable.

  2. Signaling Unreachable Code:

    • You can use never to explicitly mark code paths that should be unreachable.
    function throwError(message: string): never {
      throw new Error(message);
      // Code after this point is unreachable
    }

    TypeScript understands that throwError never returns, so it infers the type of the expression after the throw statement as never.

Key Points:

  • You cannot assign any value to a variable of type never except for the result of a function that also returns never.
  • The never type is assignable to every type, making it useful for situations where you want to narrow down types based on certain conditions.

In summary, while you may not use never as frequently as other types, it's a powerful tool for improving code correctness and maintainability by making your code's behavior more explicit.


What is the need of --incremental flag in typescript?
The --incremental flag in TypeScript is used to significantly speed up subsequent compilation times. It does this by saving information about the previous compilation process and then only re-compiling the files that have changed, or that are affected by changes.

Here's a breakdown:

How it works:

  1. First Compilation: When you compile with --incremental for the first time, TypeScript does the following:

    • Compiles your code as usual.
    • Creates a special file called tsconfig.tsbuildinfo (by default) in your output directory. This file acts as a cache, storing information about the project structure, dependencies, and compilation outputs.
  2. Subsequent Compilations: On subsequent compilations with --incremental:

    • TypeScript reads the tsconfig.tsbuildinfo file.
    • It compares the current project state (files modified, added, deleted) to the information in the cache.
    • Only the files that have changed or are affected by the changes are recompiled.
    • The tsconfig.tsbuildinfo file is updated to reflect the latest compilation state.

Example:

Let's say you have a project with three files:

  • main.ts
  • utils.ts
  • models.ts

Scenario: You make a change in utils.ts and then compile your project.

Without --incremental: TypeScript would recompile all three files (main.ts, utils.ts, and models.ts) even though only utils.ts was modified.

With --incremental: TypeScript would only recompile utils.ts. If the change in utils.ts affected how main.ts uses it, then main.ts would be recompiled as well. models.ts would remain untouched.

How to use --incremental:

  1. Command Line:

    tsc --incremental
  2. tsconfig.json:

    {
      "compilerOptions": {
        "incremental": true,
        "outDir": "./dist" // Specify your output directory
      }
    }

Benefits:

  • Faster compilation times: Especially noticeable in large projects where recompiling everything takes significant time.
  • Improved development workflow: Enables faster feedback loops during development.

Things to Note:

  • The tsconfig.tsbuildinfo file is essential for incremental compilation. Don't delete it unless you want to force a full recompilation.
  • If you make significant changes to your project structure or dependencies, it's a good idea to do a clean build (delete the tsconfig.tsbuildinfo file and recompile) to ensure everything is in sync.
  • Incremental compilation is generally a good practice for most TypeScript projects, particularly larger ones.

What is the difference between private keyword and private fields in typescript?
Let's break down the difference between the private keyword and TypeScript's newer private fields (using the # symbol).

Traditional private Keyword

  • How it works: Historically, TypeScript used the private keyword to enforce access control within classes.
  • Compile-time check: The private keyword is primarily a compile-time feature. TypeScript's type checker prevents you from accessing private members from outside the class they are defined in.
  • JavaScript output: When TypeScript code is transpiled to JavaScript, the private modifier is removed. This is because, until recently, JavaScript itself didn't have a native way to enforce private members.

Example:

class BankAccount {
  private balance: number; 

  constructor(initialBalance: number) {
    this.balance = initialBalance;
  }

  deposit(amount: number) {
    this.balance += amount;
  }

  withdraw(amount: number) {
    if (this.balance >= amount) {
      this.balance -= amount;
    } else {
      console.log("Insufficient funds");
    }
  }
}

const myAccount = new BankAccount(1000);
myAccount.deposit(500); 
// myAccount.balance = 0; // Error: Property 'balance' is private 

Private Fields (# Symbol)

  • True privacy: Introduced in TypeScript 3.8, private fields marked with # offer true privacy at both compile-time and runtime.
  • JavaScript support: This feature aligns with JavaScript's own implementation of private class fields, ensuring consistent behavior.
  • No access outside the class: Even within the same JavaScript object, attempts to access private fields using different techniques will result in errors.

Example:

class SecretMessage {
  #message: string;

  constructor(message: string) {
    this.#message = message;
  }

  reveal() {
    console.log(this.#message);
  }
}

const mySecret = new SecretMessage("This is confidential!");
mySecret.reveal(); // Output: "This is confidential!"
// console.log(mySecret.#message); // Error: Property '#message' is not accessible outside class 'SecretMessage'.

Key Differences Summary

Feature private Keyword # Private Fields
Privacy Level Compile-time Runtime & Compile-time
JavaScript Support No Yes
Access Restriction Within the class Strictly within the class (even on the same object)

Which to Choose?

  • For most scenarios: Use private fields (#) for enhanced privacy and future-proofing your code.
  • Legacy codebases: You might still encounter the private keyword in older TypeScript projects.

What is the benefit of import assertions features in typescript?
Import assertions in TypeScript provide a way to tell the compiler what type of module you're importing, even if the module itself doesn't have type declarations. This is particularly useful when:

  • Importing data files (JSON, CSS, etc.): TypeScript can't infer the types of data files. Assertions ensure type safety when accessing their content.
  • Using modules without type declarations: You can provide type information for JavaScript libraries lacking .d.ts files.
  • Migrating from JavaScript to TypeScript: Assertions help incrementally add type checking to your project.

Benefits in Detail:

  1. Improved Type Safety: Ensures that you're using imported data correctly, reducing runtime errors.
  2. Better Autocompletion and Type Hints: Provides better code intelligence in your IDE, making development smoother.
  3. Clearer Code Intent: Explicitly states the expected type of the import.
  4. Smoother Integration of Untyped Modules: Allows leveraging existing JavaScript libraries with type safety.

Example: Importing a JSON file

Let's say you have a data.json file:

{
  "name": "Example Data",
  "version": 1.2
}

Without import assertions, TypeScript wouldn't know the structure of this data:

import data from './data.json';

console.log(data.name); // No type checking, potential runtime error

Using an import assertion, we can specify the type:

import data from './data.json' assert { type: 'json' };

console.log(data.name); // Type checked, autocomplete works correctly

// data is now typed as:
// {
//   name: string;
//   version: number;
// } 

Explanation:

  • assert { type: 'json' } tells TypeScript to treat the import as a JSON object.
  • Now, TypeScript can type check the access to data.name and provide autocompletion.

Important Note: Import assertions are primarily a type-checking mechanism. They don't change how modules are loaded at runtime. Always ensure your environment supports loading the specific module type.


How to make a union type from a type alias or interface properties in typescript?
You can't directly create a union type from an existing type alias or interface's properties using TypeScript's built-in utility types like keyof. Here's why and how to achieve this:

Why It's Not Directly Possible:

  • Type Level Abstraction: Type aliases and interfaces act as blueprints for objects. TypeScript primarily uses them during type checking, but these types themselves don't exist at runtime. When you use keyof, you get a union of string literals representing the property names, not the types of those properties.

How to Achieve It:

  1. Mapped Types (for Modifying Existing Properties):

    interface User {
      name: string;
      age: number;
      isAdmin: boolean;
    }
    
    type UserPropertyValues = {
      [K in keyof User]: User[K]; // Map over each property and get its type
    }[keyof User]; // Index with 'keyof User' to get the union
    
    // UserPropertyValues will be string | number | boolean
    • We use a mapped type ({[K in keyof User]: ...}) to iterate over each property in the User interface.
    • For each property K, we access its type using User[K].
    • Finally, we index the mapped type using [keyof User] to get a union of all the property value types.
  2. Index Signatures (for Dynamic Properties):

    interface DataRecord {
      [key: string]: string | number | boolean; // Index signature
    }
    
    type DataRecordValue = DataRecord[string]; // string | number | boolean
    • If your type alias or interface uses an index signature, you can directly index it with the index signature's key type (string in this case) to obtain the union of possible value types.

Example:

interface Product {
  id: number;
  name: string;
  price: number;
  inStock: boolean;
}

type ProductValue = {
  [K in keyof Product]: Product[K];
}[keyof Product]; 

const productId: ProductValue = 123; 
const productName: ProductValue = "Awesome Gadget"; 
const productInStock: ProductValue = false; 

Now, ProductValue represents the union type string | number | boolean.

Key Points:

  • These methods extract a union of all possible property value types from the original type.
  • If you need to perform more complex transformations or access specific properties, you might need to use conditional types or other advanced TypeScript features.

What does the tsconfig option lib do?
The lib option in the tsconfig.json file lets you specify which built-in API declarations TypeScript should include in your project. These declarations describe the available JavaScript APIs, like DOM manipulation functions, that TypeScript uses for type checking and code completion.

How it works

Without specifying the lib option, TypeScript defaults to including declarations for the ECMAScript 5 (ES5) standard. This means you can use all the basic JavaScript features without needing extra libraries. However, modern JavaScript environments (like browsers and Node.js) offer many more APIs beyond ES5.

By using the lib option, you can tell TypeScript which APIs your project targets. This ensures:

  • Accurate type checking: TypeScript will recognize and validate your use of specific APIs.
  • Better code completion: Your editor will suggest relevant APIs based on the chosen environment.
  • Smaller bundle sizes: Only the necessary declaration files are included, potentially reducing the final bundle size of your project.

Example

Let's say you are building a web application and want to use the fetch API, which is not part of ES5. To enable its use in your TypeScript code, you need to update the lib option in your tsconfig.json:

{
  "compilerOptions": {
    "target": "es6", 
    "lib": ["es6", "dom"] 
  }
}

This configuration tells TypeScript:

  • "target": "es6": Compile the code to ES6 syntax.
  • "lib": ["es6", "dom"]: Include declaration files for:
    • es6: Enables ES6 features like let, const, arrow functions, etc.
    • dom: Enables browser-specific APIs like document, window, and the fetch function.

Now you can use fetch in your TypeScript code:

fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data));

TypeScript will recognize the fetch function and provide type checking and code completion based on its definition in the included dom declarations.

Available Libraries

TypeScript offers a wide range of library options for different environments. Some common ones include:

  • ES versions (es5, es6, es2015, esnext, etc.): Enable features from specific ECMAScript versions.
  • DOM (dom): APIs for interacting with the browser's document object model.
  • WebWorker (webworker): APIs for using web workers.
  • ES Modules (es2015.promise, es2015.iterable, etc.): Enable specific ES module features.
  • Node.js (node): APIs for Node.js environment.

You can find a complete list of available libraries in the TypeScript documentation: https://www.typescriptlang.org/tsconfig#lib

Remember to tailor the lib option to your project's target environment for optimal performance and accuracy.


How would you overload a class constructor in typescript?
Certainly, let's delve into constructor overloading in TypeScript with an explanation and an illustrative example.

Constructor Overloading in TypeScript

Constructor overloading in TypeScript allows a class to have multiple constructors with different parameter lists. This enables you to create objects of the same class in various ways, depending on the data you have available during instantiation.

Mechanism:

TypeScript, unlike languages like Java or C++, doesn't directly support multiple constructor definitions. Instead, you define a single constructor with optional parameters and type guards to handle different initialization scenarios.

Example:

class Product {
  id: number;
  name: string;
  price?: number; // Optional parameter

  constructor(id: number, name: string, price?: number) {
    this.id = id;
    this.name = name;

    // Type guard to handle optional 'price'
    if (typeof price !== 'undefined') {
      this.price = price;
    }
  }
}

// Creating products using different constructor "overloads"
const product1 = new Product(1, 'Laptop');
const product2 = new Product(2, 'Keyboard', 49.99);

console.log(product1); // { id: 1, name: 'Laptop' }
console.log(product2); // { id: 2, name: 'Keyboard', price: 49.99 }

Explanation:

  1. Optional Parameter: In the Product class, the price parameter in the constructor is marked as optional using the ?. This signifies that it may or may not be provided during object creation.

  2. Type Guard: Inside the constructor, we use a type guard (typeof price !== 'undefined') to check if the price argument was actually passed. If it was, we assign it to this.price; otherwise, it remains undefined.

Key Points:

  • You effectively have different ways to construct a Product (with or without a price) using a single constructor definition.
  • TypeScript's type system understands these different "overloads" based on the provided arguments.

Benefits:

  • Flexibility: Provides multiple construction pathways.
  • Code Cleanliness: Avoids redundant constructor definitions.

What is the result of the following expression in typescript? in a?.b.c , if a.b is null, then a.b.c will evaluate to what?
You're touching on the heart of TypeScript's optional chaining operator (?.) – its ability to gracefully handle potential errors when accessing properties of nested objects.

Explanation:

  1. Optional Chaining (?.): This operator acts as a safeguard against null or undefined values. Here's how it works:

    • If the expression to the left of ?. is null or undefined, the entire expression short-circuits and evaluates to undefined.
    • If the expression to the left of ?. has a value, the property access continues as normal.
  2. The Scenario: a?.b.c

    • The expression first checks if a exists. If a is null or undefined, the entire expression immediately becomes undefined.
    • If a exists, it then checks if a.b exists. If a.b is null or undefined, the expression again short-circuits to undefined.
    • Only if both a and a.b have values will it attempt to access c (as in a.b.c).

Example:

interface Data {
  b?: {
    c: string;
  };
}

let a1: Data | null = null;
let a2: Data = {}; 
let a3: Data = { b: { c: "Hello" } };

console.log(a1?.b?.c); // undefined (a1 is null)
console.log(a2?.b?.c); // undefined (a2.b is undefined)
console.log(a3?.b?.c); // "Hello" (a3 and a3.b both exist)

Key Benefit: Optional chaining prevents runtime errors that would normally occur when trying to access properties of null or undefined values, making your code more robust.