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:
- Using the
static
andreadonly
keywords - Using the
const
keyword with theas 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 typestring | number
. This meansvalue
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:
- Explicit Type: We declared
name
as astring
. TypeScript will enforce that only string values can be assigned toname
. - Type Inference: We didn't specify a type for
age
, but TypeScript infers it to benumber
based on the initial value30
. - 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:
- Multiple Signatures: You define two or more function signatures with the same name but different parameter lists (number of parameters or their types).
- Type Inference: TypeScript's type system analyzes the arguments passed to the overloaded function during compilation.
- Signature Matching: It selects the most specific signature that matches the provided arguments.
- 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?
- 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.
- Code Reusability: Define common logic once as a decorator and apply it to multiple properties across your codebase.
- 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:
-
Decorator Factory (
MinLength
):- Takes
minLength
as input and returns the actual decorator function. - This pattern allows you to create decorators with configurable parameters.
- Takes
-
Decorator Function:
- Receives the
target
(class prototype) andpropertyKey
(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.
- Receives the
-
Applying the Decorator (
@MinLength(5)
):- Attaches the
MinLength
decorator to thename
property of theProduct
class, enforcing a minimum length of 5 characters.
- Attaches the
What are different components of typescript?
Let's break down the core components of TypeScript and illustrate them with examples.
Fundamental TypeScript Components
-
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;
-
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" };
-
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!
-
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);
-
Enums: A way to give more meaningful names to sets of numeric values.
Example:
enum Status { Pending, Approved, Rejected } let orderStatus: Status = Status.Approved;
-
Modules: Allow you to organize your code into smaller, reusable units, making it more maintainable. You use
import
andexport
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:
-
We define a class
Rectangle
. -
Crucially, we prefix the class declaration with
export
. This keyword signals that theRectangle
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 enum
s 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:
- We define an interface
Flyable
which requires afly()
method. - We have two classes,
Bird
andPlane
, both implementing afly()
method, but without explicitly stating that they implement theFlyable
interface. - The
takeFlight
function accepts any object that has afly()
method, thanks to theFlyable
type constraint. - Both
parrot
(aBird
) andairbus
(aPlane
) can be passed totakeFlight
because they structurally match theFlyable
interface by having thefly()
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:
-
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.
-
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.
-
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:
-
Vehicle
Class: A basic class for all vehicles. -
EngineMixin
andElectricMixin
: These are our mixin classes. They contain functionalities (methods) related to engines and electric systems. -
Applying Mixins: We use a helper function
applyMixins
to apply the mixins to our concrete classes (Car
andElectricCar
). This function essentially copies the properties and methods from the mixin prototypes onto the prototypes of our classes. -
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 orObject.keys()
. This helps you create "private" object properties.
Use Cases
-
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" }
-
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:
-
const point = [10, 20] as const;
:- We declare
point
usingconst
, which prevents reassignment of the entire tuple variable. as const
is crucial: without it, TypeScript would inferpoint
asnumber[]
. The assertion tells TypeScript to treat the array literal as a tuple with specific element types and length.
- We declare
-
Error on Modification Attempts:
- Trying to change
point[0]
or use array methods likepush
results in compile-time errors, as TypeScript enforces the read-only nature.
- Trying to change
-
Type Inference:
- The type of
point
becomesreadonly [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.
- The first element is always the number
- The type of
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.
-
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 thecompilerOptions
to automatically generate declaration files during compilation.
- You compile "my-library" normally, generating a declaration file (
-
tsconfig.json
in "my-app":- You add a
"references"
array to thecompilerOptions
, specifying the path to "my-library"'stsconfig.json
. - You use
import
statements to bring in modules from "my-library" just like any other dependency.
- You add a
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.
- Functions that always throw errors: If a function's sole purpose is to throw an error and never return normally, its return type is
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 like0
,''
, orNaN
- only strictlynull
orundefined
.
2. Behavior:
- Optional Chaining (
?.
): If any part of the chain encountersnull
orundefined
, the entire expression evaluates toundefined
without throwing an error. - Nullish Coalescing (
??
): Returns the first operand if it's notnull
orundefined
. Otherwise, it returns the second operand (the default value).
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 encounteringnull
orundefined
.
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 tofalse
, the entire expression is guaranteed to befalse
. Consequently, the right operand is not evaluated. -
||
(OR): If the left operand evaluates totrue
, the entire expression is guaranteed to betrue
. 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:
-
When
checkValue(12)
is called,value !== null
istrue
, so the evaluation proceeds tovalue > 10
, which is alsotrue
. -
However, when
checkValue(null)
is called,value !== null
isfalse
. Due to short-circuiting with&&
, the expressionvalue > 10
is never evaluated (which would cause an error ifvalue
were indeednull
).
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?
-
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.
- Private fields are declared using a
-
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.
- Unlike
-
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 ofthis
might change (e.g., in event handlers or callbacks).
- Within the class, even if you're inside a method or nested function, you cannot access private fields using
-
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).
-
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.
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:
-
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)
-
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)
-
Array.isArray()
: Specifically for checking if a value is an array.let arr = [1, 2, 3]; console.log(Array.isArray(arr)); // true
-
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
ornumber
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.
// 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 theUser
interface and adding anisAdmin
property.Employee
is an interface extending theUser
interface and adding anemployeeId
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:
-
Function Signature:
calculateArea(width: number, height: number): number
width: number
: Thewidth
parameter must be a number.height: number
: Theheight
parameter must be a number.: number
: The function's return type is a number.
-
Correct Usage:
calculateArea(10, 5)
passes two numbers as arguments, satisfying the type requirements. -
Type Error:
calculateArea("10", 5)
attempts to pass a string ("10"
) for thewidth
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 thelength
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 usemap()
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 thelength
property. - If you need to populate the array with initial values based on their index, use
Array.from()
withmap()
. - 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 fornull
orundefined
. - Loose Equality (
==
): This operator allows for type coercion. While it can be convenient for checkingnull
andundefined
together, it might lead to unexpected behavior in other comparisons. - Optional Chaining (
?.
): This operator allows safe property access on potentiallynull
orundefined
objects, avoiding runtime errors. - Nullish Coalescing Operator (
??
): This operator provides a concise way to handle default values when dealing withnull
orundefined
.
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:
-
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.
-
Error Prevention: Crucially,
override
helps prevent accidental errors. If you useoverride
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:
-
TimestampMixinConstructor
: This type alias defines the structure of our mixin's constructor. It takes a generic typeT
(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 addedcreatedAt
property of typeDate
.
- Accepts the same parameters as the base class's constructor (
-
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.
- It takes a class (
-
Usage: We use
withTimestamp
to enhance theUser
class, creating a newTimeStampedUser
class. Instances ofTimeStampedUser
now have both thename
property from the originalUser
class and the addedcreatedAt
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 torequire()
in Node.js)..then((module) => ...)
: Since dynamic imports are asynchronous, they return a Promise. We use.then()
to handle the resolved module.module
: Themodule
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:
- We use
async/await
to handle the Promise returned by the dynamic import. - The
factorial
function becomes available only after themathUtils.ts
module is loaded successfully. - 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
: Whilevoid
indicates the absence of a return value (a function might return nothing, orundefined
),never
means a function will never return at all. -
never
vs.undefined
ornull
:undefined
andnull
are values that can exist in your code, potentially indicating the absence of data.never
signifies that a value cannot logically exist.
Example Scenarios
-
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 asnever
because it always throws an error, preventing it from ever reaching a normal return statement. -
Infinite Loops:
function infiniteLoop(): never { while (true) { // Do something forever... } }
infiniteLoop
is also typed asnever
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 reachablereturn
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:
curriedVolume
: This function takes thelength
argument and returns a new function.- Returned Function (1st level): This function takes the
width
argument and returns another function. - 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:
PromiseReturnType<T>
: This is our generic type. It takes another typeT
as an argument.T extends (...args: any[]) => Promise<infer R>
: This is a constraint. It ensures that the typeT
passed toPromiseReturnType
is a function that:- Accepts any number of arguments (
...args: any[]
). - Returns a
Promise
.
- Accepts any number of arguments (
infer R
: This is the magic!infer R
tells TypeScript to infer the type of the value thePromise
resolves to and temporarily name itR
.= R
: Finally, we tell TypeScript that thePromiseReturnType
itself should be the inferred typeR
.
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 theany
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 withunknown
. -
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
) orany
, 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
-
interface ShoppingCartItem
: This defines the structure of each item in your cart. -
[itemName: string]: ShoppingCartItem
: This is the index signature. Let's break it down:[itemName: string]
: This indicates that the keys of thecart
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 theShoppingCartItem
interface.
-
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
andprice
).
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
-
[key: string]: T
: This is the index signature. It states that ourDataStore
class can have properties with keys of typestring
and values of typeT
(a generic type parameter, making our class flexible to store different data types). -
addData
andgetData
: 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
-
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);
-
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
-
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:
-
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
-
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
-
-
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:
-
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.
-
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. -
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 typeJQueryStatic
.interface JQueryStatic ...
: This interface defines the shape of theJQueryStatic
object (what$
points to), including methods likeajax
.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 ofMyEnum
without generating any JavaScript output for it. This lets you useMyEnum
for type safety, but at runtime, you're working directly with the JavaScript object frommy-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 aboutMyConstEnum
. - 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 regulardeclare 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:
-
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 thedefault
case would now be reachable. - When a function is annotated to return
-
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 asnever
. - You can use
Key Points:
- You cannot assign any value to a variable of type
never
except for the result of a function that also returnsnever
. - 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:
-
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.
-
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.
- TypeScript reads the
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
:
-
Command Line:
tsc --incremental
-
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 accessingprivate
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:
- Improved Type Safety: Ensures that you're using imported data correctly, reducing runtime errors.
- Better Autocompletion and Type Hints: Provides better code intelligence in your IDE, making development smoother.
- Clearer Code Intent: Explicitly states the expected type of the import.
- 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:
-
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 theUser
interface. - For each property
K
, we access its type usingUser[K]
. - Finally, we index the mapped type using
[keyof User]
to get a union of all the property value types.
- We use a mapped type (
-
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.
- If your type alias or interface uses an index signature, you can directly index it with the index signature's key type (
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 likelet
,const
, arrow functions, etc.dom
: Enables browser-specific APIs likedocument
,window
, and thefetch
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:
-
Optional Parameter: In the
Product
class, theprice
parameter in the constructor is marked as optional using the?
. This signifies that it may or may not be provided during object creation. -
Type Guard: Inside the constructor, we use a type guard (
typeof price !== 'undefined'
) to check if theprice
argument was actually passed. If it was, we assign it tothis.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:
-
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
?.
isnull
orundefined
, the entire expression short-circuits and evaluates toundefined
. - If the expression to the left of
?.
has a value, the property access continues as normal.
- If the expression to the left of
-
The Scenario:
a?.b.c
- The expression first checks if
a
exists. Ifa
isnull
orundefined
, the entire expression immediately becomesundefined
. - If
a
exists, it then checks ifa.b
exists. Ifa.b
isnull
orundefined
, the expression again short-circuits toundefined
. - Only if both
a
anda.b
have values will it attempt to accessc
(as ina.b.c
).
- The expression first checks if
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.