What is TypeScript and how does it differ from JavaScript ?
Here’s a breakdown of TypeScript and its key differences from JavaScript:
TypeScript:
- Superset of JavaScript: It builds upon JavaScript, adding optional static typing and other features.
- Compilation: TypeScript code (.ts files) is transpiled into JavaScript (.js) before execution.
- Static Typing: Variables, functions, and objects are declared with specific types, enabling type checking during development to catch potential errors early.
- Interfaces: Abstract blueprints for classes, defining expected properties and methods, promoting code reusability and consistency.
- Classes: A way to model objects with encapsulated properties and methods, supporting object-oriented programming principles.
- Namespaces: A mechanism to organize code into logical groups, avoiding naming conflicts and improving code modularity.
- Decorators: A way to modify classes and functions with metadata, enabling features like dependency injection and class inheritance.
Key Differences from JavaScript:
- Static Typing: TypeScript enforces type safety, while JavaScript is dynamically typed, allowing type mismatches to occur at runtime.
- Interfaces: TypeScript supports interfaces, while JavaScript relies on prototype-based inheritance for object structuring.
- Classes: TypeScript has built-in class syntax, while JavaScript’s classes are syntactic sugar over prototypes.
- Namespaces: TypeScript offers namespaces for code organization, while JavaScript uses modules (ES6 and later).
- Decorators: TypeScript supports decorators, a feature not natively available in JavaScript.
What are enums ?
In TypeScript, enums are a way to define a set of named constants. They allow you to create a collection of related values that can be assigned to variables or used as a type. Enums provide a convenient way to work with a fixed set of values in a type-safe manner.
1. Declaration: Use the enum
keyword followed by the enum name and a list of constant values within curly braces.
enum Color { Red, Green, Blue }
2. Member Access: Access enum members using dot notation:
let color: Color = Color.Green; console.log(color); // Output: 1 (numeric value) console.log(Color[1]); // Output: "Green" (string value)
3. Numeric and String-Based Enums:
- Numeric Enums: Automatically assigned numeric values starting from 0 (or a specified value).
- String-Based Enums: Use string literals as member values:
enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT" }
4. Const Enums: Optimized for runtime performance by replacing enum values with their literal values at compile time:
const enum DaysOfWeek { Monday, Tuesday, Wednesday, Thursday, Friday }
5. Reverse Mappings: Access enum values by their numeric or string counterparts using bracket notation:
let day = DaysOfWeek[3]; // day will be "Thursday"
What are type annotations ?
In TypeScript, type annotations are used to specify the data type of a variable, function return type, or function parameter type. They provide the TypeScript compiler with information about the type of data a variable can store, which can help catch type-related errors during development.
Here is an example of how to use type annotations in TypeScript:
let age: number = 30; let name: string = "John"; let isStudent: boolean = true;
In the above example, we define three variables age
, name
, and isStudent
, and use type annotations to specify their types as number
, string
, and boolean
, respectively.
Type annotations can also be used with function parameters and return types. Here is an example:
function addNumbers(a: number, b: number): number { return a + b; }
In the above example, we define a function addNumbers
that takes two parameters of type number
and returns a value of type number
.
How do you declare different types in TypeScript ?
Here’s a guide to declaring different types in TypeScript:
1. Primitives:
Syntax: let variableName: PrimitiveType;
let name: string = "Bard"; let age: number = 42; let isLoggedIn: boolean = true;
2. Arrays:
- Syntax:
let arrayName: Array<Type>;
orlet arrayName: Type[];
let numbers: number[] = [1, 2, 3]; let names: string[] = ["Alice", "Bob", "Charlie"];
3. Objects:
- Syntax:
let objectName: { propertyName: Type, ... };
let person: { name: string; age: number; } = { name: "Bard", age: 42 }; let product: { id: number; name: string; price: number; } = { id: 1, name: "Widget", price: 9.99 };
4. Tuples:
- Syntax:
let tupleName: [Type1, Type2, ...];
let coordinates: [number, number] = [10, 20]; let user: [string, number, boolean] = ["Bard", 42, true];
5. Functions:
- Syntax:
let functionName: (parameters: Types) => returnType;
let add: (x: number, y: number) => number = (x, y) => x + y; let greet: (name: string) => void = (name) => console.log("Hello, " + name);
6. Enums:
- Syntax:
enum EnumName { Value1, Value2, ... };
enum Color { Red, Green, Blue } let color: Color = Color.Blue;
7. Type Aliases:
- Syntax:
type AliasName = Type;
type Name = string; type Point = [number, number]; let fullName: Name = "Bard"; let origin: Point = [0, 0];
8. Union Types:
- Syntax:
let variableName: Type1 | Type2 | ...;
let value: string | number = "hello"; let response: boolean | string = true;
9. Intersection Types:
- Syntax:
let variableName: Type1 & Type2;
let user: Person & Admin = { name: "Bard", age: 42, isAdmin: true };
10. Literal Types:
- Syntax:
let variableName: "Value1" | "Value2" | ...;
let direction: "left" | "right" | "up" | "down" = "up";
Remember: TypeScript’s type system enhances code clarity, maintainability, and error prevention. Choose appropriate types for your variables and data structures to leverage these benefits.
How is type inference used in TypeScript ?
Type inference is a feature in TypeScript that allows the compiler to automatically determine the type of a variable based on its value. This means that you don’t have to explicitly specify the type of a variable in many cases, as TypeScript can infer it for you.
Here are the examples of how type inference works in TypeScript:
// Inferred type: number let x = 3; // Inferred type: string let y = "hello"; // Inferred type: boolean let z = true; // Inferred type: number[] let numbers = [1, 2, 3]; // Type inferred as (number | string | boolean)[] let items = [10, "hello", true]; // Inferred type: { name: string, age: number } let person = { name: "John", age: 30 }; // Return type inferred as number function add(x: number, y: number): number { return x + y; } function greet(person: { name: string }) { console.log("Hello, " + person.name); } greet({ name: "Alice" }); // Type inferred as { name: string } function logMessage(message: string) { console.log(message); } let message = 10; logMessage(message); // Error: Argument of type 'number' is not assignable to parameter of type 'string'
Differences between interfaces and types.
Interfaces: Defines contracts for object shapes, specifying properties and methods that objects must have.
interface Person { name: string; age: number; greet(): void; }
Types: Creates new names for existing types, including primitive types, unions, intersections, tuples, and more.
type Name = string; type ID = number; type Point = [number, number]; type User = { name: Name; id: ID };
- Interfaces are used to define the shape of an object. They can only be used to describe object types, and can include methods, properties, and index signatures.
- Types are used to define a type alias for a specific type. They can be used to describe any type, including primitive types, union types, and tuple types.
- Interfaces can be extended to create new interfaces that inherit the properties of the parent interface.
- Types can be used to create aliases for existing types, which can make code more readable and easier to maintain.
- Interfaces can be used to define contracts for objects, which can help ensure that objects conform to a specific structure.
- Types can be used to create complex types that are difficult to express using interfaces.
What are generics ?
Generics in TypeScript are a powerful feature that allows you to create flexible, reusable components that can work with different data types without compromising type safety. By using generics, you can create functions, classes, and interfaces that can work with a variety of types, rather than a specific type.
Here are the examples of how to define a generic function in TypeScript:
//Generic Type Parameters function identity<T>(arg: T): T { return arg; } let output = identity<string>("hello world"); console.log(output); // Output: "hello world" //Generic Interfaces interface Pair<T, U> { first: T; second: U; } let pair: Pair<number, string> = { first: 1, second: "hello" }; console.log(pair); // Output: { first: 1, second: "hello" } //Generic Classes class GenericNumber<T extends number> { private value: T; constructor(value: T) { this.value = value; } get(): T { return this.value; } } let num = new GenericNumber<number>(42); console.log(num.get()); // Output: 42 //Generic Type Aliases type Callback<T> = (value: T) => void; function logValue<T>(value: T, callback: Callback<T>): void { console.log(value); callback(value); } logValue("hello world", (value: string) => { console.log(`The length of "${value}" is ${value.length}`); });
What are decorators ?
Decorators are a way to add both annotations and a meta-programming syntax for class declarations and members. Decorators are a powerful feature that allows you to modify the behavior of classes, methods, properties, and parameters dynamically. They provide a way to attach metadata to these elements, enabling various functionalities.
Here’s a breakdown of decorators:
Syntax:
- Use the
@expression
syntax before the declaration you want to decorate. - The
expression
must evaluate to a function that acts as the decorator.
Types of Decorators:
- Class Decorators: Applied to class declarations.
- Property Decorators: Applied to property declarations.
- Method Decorators: Applied to method declarations.
- Parameter Decorators: Applied to parameter declarations.
- Accessor Decorators: Applied to getter and setter methods.
// Class decorator function sealed(target: Function) { // Prevent further subclassing Object.seal(target); } @sealed class Person { // ... } // Property decorator function readonly(target: any, key: string) { // Make the property read-only Object.defineProperty(target, key, { writable: false }); } class Product { @readonly id: number; // ... } // Method decorator function log(target: any, key: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; descriptor.value = function(...args: any[]) { console.log(`Calling method ${key} with arguments: ${args}`); originalMethod.apply(this, args); }; } class Logger { @log greet(name: string) { console.log(`Hello, ${name}!`); } }
What are namespaces ?
In TypeScript, a namespace is a way to organize code into logical groups and avoid naming collisions between identifiers. Namespaces provide a way to group related code into a single namespace or module so that we can manage, reuse and maintain our code easily.
Here is an example of how to define a namespace in TypeScript:
namespace MyNamespace { export interface MyInterface { // Interface code here } export class MyClass { // Class code here } export function myFunction() { // Function code here } }
In the above example, we define a namespace MyNamespace
that contains an interface MyInterface
, a class MyClass
, and a function myFunction
. We use the export
keyword to make these entities visible outside the namespace.
To use the entities defined in the namespace, we can use the following syntax:
let myObject: MyNamespace.MyClass = new MyNamespace.MyClass();
In the above example, we create an instance of the MyClass
class defined in the MyNamespace
namespace and assign it to a variable myObject
.
How do you achieve inheritance in TypeScript classes?
In TypeScript, inheritance can be achieved using the extends
keyword. The extends
keyword is used to create a subclass that inherits properties and methods from a parent class. Here is an example of how to use inheritance in TypeScript:
class Animal { name: string; constructor(name: string) { this.name = name; } move(distanceInMeters: number = 0) { console.log(`${this.name} moved ${distanceInMeters}m.`); } } class Snake extends Animal { constructor(name: string) { super(name); } move(distanceInMeters = 5) { console.log("Slithering..."); super.move(distanceInMeters); } } class Horse extends Animal { constructor(name: string) { super(name); } move(distanceInMeters = 45) { console.log("Galloping..."); super.move(distanceInMeters); } } let sam = new Snake("Sammy the Python"); let tom: Animal = new Horse("Tommy the Palomino"); sam.move(); tom.move(34);
In the above example, we define a Animal
class with a name
property and a move
method. We then define two subclasses Snake
and Horse
that extend the Animal
class. The Snake
class overrides the move
method to add custom behavior, while the Horse
class overrides the move
method to add different custom behavior. We create instances of both subclasses and call their move
methods.
Explain access modifiers (public, private, protected) in TypeScript classes ?
In TypeScript, access modifiers are used to control the visibility of class members such as properties and methods. TypeScript provides three access modifiers: public
, private
, and protected
.
- public: Members marked as
public
can be accessed from anywhere, including outside the class. - private: Members marked as
private
can only be accessed within the class they are defined. - protected: Members marked as
protected
can be accessed within the class they are defined and any subclasses.
Here is an example of how to use access modifiers in TypeScript:
class Person { public name: string; private age: number; protected address: string; constructor(name: string, age: number, address: string) { this.name = name; this.age = age; this.address = address; } public getAge(): number { return this.age; } } class Employee extends Person { private salary: number; constructor(name: string, age: number, address: string, salary: number) { super(name, age, address); this.salary = salary; } public getSalary(): number { return this.salary; } } let person = new Person("John", 30, "123 Main St"); console.log(person.name); // Output: "John" console.log(person.age); // Compile error console.log(person.address); // Compile error let employee = new Employee("Jane", 25, "456 Elm St", 50000); console.log(employee.name); // Output: "Jane" console.log(employee.address); // Compile error console.log(employee.getSalary()); // Output: 50000
In the above example, we define a Person
class with public
, private
, and protected
members. We then define an Employee
class that extends the Person
class and adds a private
member. We create instances of both classes and access their members using dot notation.
What is the difference between any and unknown in TypeScript?
In TypeScript, any
and unknown
are both types that can hold any value. However, there are some differences between them.
any
is a type that can hold any value, and it is often used when the type of a value is not known at compile time. any
is not type-safe, meaning that you can perform any operation on a value of type any
, regardless of whether that operation is valid for that value. This can lead to runtime errors if you perform an operation on an any
value that is not valid for that value.
unknown
is a type that can also hold any value, but it is type-safe. This means that you cannot perform any operation on a value of type unknown
without first checking its type. This makes unknown
a safer alternative to any
when you need to work with values of unknown types.
// `any` allows anything without checks let anyValue: any = 10; anyValue = "hello"; // No error anyValue.toUpperCase(); // Allowed, but might fail at runtime // `unknown` requires checks before usage let unknownValue: unknown = "world"; unknownValue.toUpperCase(); // Error: Cannot perform operations on 'unknown' type // Narrowing `unknown` to a specific type if (typeof unknownValue === "string") { const strValue = unknownValue as string; // Type assertion strValue.toUpperCase(); // Now allowed }