TYPESCRIPT Tutorial Course
Introduction to TypeScript
What is TypeScript?
TypeScript is a statically typed superset of JavaScript developed by Microsoft. It adds optional static types to the language, enabling developers to catch errors early through type checking, and to use powerful tools and workflows that enhance productivity. TypeScript code is transpiled to standard JavaScript, which means it can run on any environment that JavaScript can run on.
Benefits of TypeScript over JavaScript
- Static Typing
- Error Detection: TypeScript helps detect errors at compile-time rather than runtime, which can save time and reduce bugs.
- Type Safety: Ensures variables are used consistently, reducing runtime errors due to type mismatches.
- Improved Development Experience
- Enhanced Tooling: Modern IDEs provide better autocompletion, navigation, and refactoring capabilities for TypeScript.
- Intellisense: Offers intelligent code completion and better code suggestions.
- Better Code Maintainability
- Self-Documenting Code: Types and interfaces act as documentation for your code, making it easier to understand.
- Refactoring: Safer and more efficient refactoring due to static type checking.
- Early Bug Detection
- Compile-Time Checks: Errors are caught during compilation, which prevents many runtime errors.
- Strict Mode: TypeScript’s strict mode can enforce more rigorous checks, reducing the risk of common JavaScript pitfalls.
- Scalability
- Large Codebases: Helps manage and scale large codebases by providing structure and clear contracts through types and interfaces.
- Team Collaboration: Makes it easier for teams to collaborate, as the types act as a clear contract for function and method usage.
- Modern JavaScript Features
- Future-Proofing: Allows the use of modern JavaScript features and syntax, which are transpiled to older JavaScript versions for compatibility.
- ES6+ Features: Supports classes, modules, arrow functions, async/await, and more.
- Compatibility
- JavaScript Compatibility: Any valid JavaScript is also valid TypeScript, making it easy to integrate TypeScript gradually into existing JavaScript projects.
- Interoperability: Works well with existing JavaScript libraries and frameworks.
- Improved Debugging
- Source Maps: Generates source maps, which make it easier to debug TypeScript code by mapping the transpiled JavaScript code back to the original TypeScript source.
- Community and Ecosystem
- Strong Community: A large and active community means plenty of resources, libraries, and tools are available.
- Definitively Typed Libraries: Many popular libraries have type definitions available, improving integration and development speed.
- Integration with Build Tools
- Build System Integration: Seamless integration with popular build tools like Webpack, Babel, and various task runners.
Setting up the TypeScript environment
Please follow below steps to setup typescript environment. In July 2024, node supports running typescript code without any additional setup. So you might want to skip below steps.
- Download and install Visual Studio Code from the official website.
- Download and install NodeJS from the official website. Node.js comes with npm (Node Package Manager), which you will use to install TypeScript.
- Open your terminal or command prompt and create a new directory for your project. Navigate into this directory:
1mkdir my-typescript-project 2cd my-typescript-project
- Initialize a New Node.js Project
npm init -y
- Install TypeScript Locally
npm install --save-dev typescript
- Create a tsconfig.json File
1{ 2 "compilerOptions": { 3 "target": "es6", 4 "module": "commonjs", 5 "strict": true, 6 "esModuleInterop": true, 7 "skipLibCheck": true, 8 "forceConsistentCasingInFileNames": true 9 }, 10 "include": [ 11 "src/**/*" 12 ], 13 "exclude": [ 14 "node_modules" 15 ] 16}
- Create project structure
mkdir src
- Create a TypeScript File
1const username: string = "JohnDoe"; 2const age: number = 30; 3const isActive: boolean = true; 4 5const greet = (name: string): string => { 6 return `Hello, ${name}!`; 7}; 8 9console.log(greet(username));
- Compile typescript
npx tsc
- Open your project in VSCode: Open VSCode and open your project directory and then Install VSCode Extensions - TypeScript and JavaScript Language Features (built-in with VSCode), ESLint (for linting, if you plan to use it), Prettier (for code formatting)
- Add Scripts to package.json
1"scripts": { 2 "build": "tsc", 3 "start": "node dist/index.js" 4}
Basic Types
Primitive types: number, string, boolean
1// number type
2let age: number = 30;
3let height: number = 5.9;
4let weight: number = 75;
5
6// string type
7let firstName: string = "John";
8let lastName: string = "Doe";
9let fullName: string = `${firstName} ${lastName}`;
10
11// boolean type
12let isActive: boolean = true;
13let isVerified: boolean = false;
14
15// Functions demonstrating usage of these types
16
17// Function to calculate Body Mass Index (BMI)
18const calculateBMI = (weight: number, height: number): number => {
19 return weight / (height * height);
20}
21
22// Function to greet a user
23const greet = (name: string, isVerified: boolean): string => {
24 return `Hello, ${name}! Your verification status is: ${isVerified ? "Verified" : "Not Verified"}`;
25}
26
27// Usage of the functions
28let bmi: number = calculateBMI(weight, height);
29let greeting: string = greet(fullName, isVerified);
30
31console.log(`Age: ${age}`);
Arrays and tuples
1// Arrays
2let numbers: number[] = [1, 2, 3, 4, 5];
3let names: string[] = ["Alice", "Bob", "Charlie"];
4let isActiveArray: boolean[] = [true, false, true];
5
6// Function to print all elements of a number array
7const printNumbers = (nums: number[]): void => {
8 nums.forEach(num => console.log(num));
9}
10
11// Function to print all names in an array
12const printNames = (names: string[]): void => {
13 names.forEach(name => console.log(name));
14}
15
16// Using the functions
17printNumbers(numbers);
18printNames(names);
19
20// Tuples
21let person: [string, number, boolean] = ["Alice", 25, true];
22let coordinates: [number, number] = [50.123, 8.543];
23
24// Function to print tuple
25const printPersonInfo = (person: [string, number, boolean]): void => {
26 console.log(`Name: ${person[0]}, Age: ${person[1]}, Is Active: ${person[2]}`);
27}
28
29// Function to print coordinates
30const printCoordinates = (coords: [number, number]): void => {
31 console.log(`Latitude: ${coords[0]}, Longitude: ${coords[1]}`);
32}
33
34// Using the functions
35printPersonInfo(person);
36printCoordinates(coordinates);
Enum types
1// Enum definition
2enum Color {
3 Red,
4 Green,
5 Blue
6}
7
8// Function using enum
9const getColorName = (color: Color): string => {
10 switch (color) {
11 case Color.Red:
12 return "Red";
13 case Color.Green:
14 return "Green";
15 case Color.Blue:
16 return "Blue";
17 default:
18 return "Unknown color";
19 }
20}
21
22// Using the enum and function
23const myColor: Color = Color.Green;
24console.log(`My color is: ${getColorName(myColor)}`);
25
26// Enum with string values
27enum Direction {
28 Up = "UP",
29 Down = "DOWN",
30 Left = "LEFT",
31 Right = "RIGHT"
32}
33
34// Function using string enum
35const getDirectionName = (direction: Direction): string => {
36 switch (direction) {
37 case Direction.Up:
38 return "Up";
39 case Direction.Down:
40 return "Down";
41 case Direction.Left:
42 return "Left";
43 case Direction.Right:
44 return "Right";
45 default:
46 return "Unknown direction";
47 }
48}
49
50// Using the string enum and function
51const myDirection: Direction = Direction.Left;
52console.log(`My direction is: ${getDirectionName(myDirection)}`);
Any, Unknown, Void, and Never types
1// any type
2let anyValue: any = "This is a string";
3anyValue = 42; // No type error
4
5// unknown type
6let unknownValue: unknown = "Now I am unknown";
7// Type checking is required
8if (typeof unknownValue === "string") {
9 console.log(unknownValue.toUpperCase()); // NOW I AM UNKNOWN
10}
11
12// void type
13function logMessage(message: string): void {
14 console.log(message);
15}
16logMessage("Logging a message!"); // Logging a message!
17
18// never type
19function failWithError(message: string): never {
20 throw new Error(message);
21}
22// failWithError("This is an error"); // Uncaught Error: This is an error
23
24function infiniteLoop(): never {
25 while (true) {
26 console.log("Looping forever...");
27 }
28}
29// infiniteLoop(); // Looping forever...
Interfaces
Defining and implementing interfaces
1// Define an interface
2interface Person {
3 name: string;
4 age: number;
5 isStudent: boolean;
6}
7
8// Use the interface in a function
9function greet(person: Person): string {
10 return `Hello, ${person.name}! You are ${person.age} years old and it is ${person.isStudent ? '' : 'not '}true that you are a student.`;
11}
12
13// Create an object that adheres to the interface
14const john: Person = {
15 name: "John Doe",
16 age: 25,
17 isStudent: true
18};
19
20// Call the function with the object
21console.log(greet(john)); // Hello, John Doe! You are 25 years old and it is true that you are a student.
Optional and readonly properties
1// Define an interface with optional and readonly properties
2interface Person {
3 readonly id: number; // Readonly property
4 name: string;
5 age: number;
6 isStudent?: boolean; // Optional property
7}
8
9// Use the interface in a function
10function describePerson(person: Person): string {
11 let description = `ID: ${person.id}, Name: ${person.name}, Age: ${person.age}`;
12 if (person.isStudent !== undefined) {
13 description += `, Student: ${person.isStudent}`;
14 }
15 return description;
16}
17
18// Create an object that adheres to the interface
19const alice: Person = {
20 id: 1,
21 name: "Alice",
22 age: 30,
23 isStudent: true
24};
25
26const bob: Person = {
27 id: 2,
28 name: "Bob",
29 age: 22
30};
31
32// Attempting to change the readonly property will cause an error
33// alice.id = 3; // Error: Cannot assign to 'id' because it is a read-only property
34
35// Call the function with the objects
36console.log(describePerson(alice)); // ID: 1, Name: Alice, Age: 30, Student: true
37console.log(describePerson(bob)); // ID: 2, Name: Bob, Age: 22
Extending interfaces
In TypeScript, interfaces can extend other interfaces, allowing you to create a new interface that inherits properties from one or more existing interfaces. This is useful for building more complex types from simpler ones.1// Define the Person interface
2interface Person {
3 name: string;
4 age: number;
5 address?: string; // Optional property
6}
7
8// Extend the Person interface to create the Employee interface
9interface Employee extends Person {
10 employeeId: number;
11 department: string;
12}
13
14// Create an object adhering to the Employee interface
15const employee: Employee = {
16 name: "Alice",
17 age: 30,
18 address: "123 Main St",
19 employeeId: 1,
20 department: "Engineering"
21};
22
23// Function to get employee details
24function getEmployeeDetails(emp: Employee): string {
25 return `Name: ${emp.name}, Age: ${emp.age}, Department: ${emp.department}`;
26}
27
28// Log the employee details
29console.log(getEmployeeDetails(employee)); // Name: Alice, Age: 30, Department: Engineering
Classes
Defining classes and constructors
1// Define a class named `Person`
2class Person {
3 // Define properties with optional visibility modifiers
4 public name: string;
5 private age: number;
6 protected gender: string;
7
8 // Constructor to initialize properties
9 constructor(name: string, age: number, gender: string) {
10 this.name = name;
11 this.age = age;
12 this.gender = gender;
13 }
14
15 // Method to return a greeting
16 public greet(): string {
17 return `Hello, my name is ${this.name}.`;
18 }
19
20 // Method to get the age (public)
21 public getAge(): number {
22 return this.age;
23 }
24
25 // Method to set the age (public)
26 public setAge(age: number): void {
27 this.age = age;
28 }
29}
30
31// Create an instance of the Person class
32const person = new Person("Alice", 30, "Female");
33
34// Use methods of the Person class
35console.log(person.greet()); // Output: Hello, my name is Alice.
36console.log(person.getAge()); // Output: 30
37
38// Set a new age
39person.setAge(31);
40console.log(person.getAge()); // Output: 31
Getters and setters
In TypeScript, you can use getters and setters to control the access to properties of a class. Getters and setters allow you to define special methods that are executed when you access or modify a property, which can be useful for validation, transformation, or encapsulation of the property values.1class Person {
2 private _age: number;
3
4 // Constructor to initialize the age
5 constructor(age: number) {
6 this._age = age;
7 }
8
9 // Getter for the age property
10 public get age(): number {
11 return this._age;
12 }
13
14 // Setter for the age property
15 public set age(value: number) {
16 if (value < 0) {
17 console.error("Age cannot be negative.");
18 } else {
19 this._age = value;
20 }
21 }
22}
23
24// Create an instance of the Person class
25const person = new Person(25);
26
27// Use the getter to access the age
28console.log(person.age); // Output: 25
29
30// Use the setter to modify the age
31person.age = 30;
32console.log(person.age); // Output: 30
33
34// Try setting an invalid age
35person.age = -5; // Output: Age cannot be negative.
36console.log(person.age); // Output: 30 (unchanged due to validation)
Inheritance and abstract classes
In TypeScript, inheritance allows you to create a new class based on an existing class, reusing and extending its functionality. Abstract classes provide a way to define a base class that cannot be instantiated directly but can be extended by other classes.1// Define an abstract class
2abstract class Animal {
3 // Abstract method that must be implemented by subclasses
4 abstract makeSound(): void;
5
6 // Non-abstract method with implementation
7 move(): void {
8 console.log("The animal moves.");
9 }
10}
11
12// Define a subclass that extends the abstract class
13class Dog extends Animal {
14 // Implement the abstract method from the base class
15 makeSound(): void {
16 console.log("Woof Woof");
17 }
18}
19
20// Define another subclass
21class Cat extends Animal {
22 // Implement the abstract method from the base class
23 makeSound(): void {
24 console.log("Meow Meow");
25 }
26}
27
28// Create instances of the subclasses
29const dog = new Dog();
30const cat = new Cat();
31
32// Call methods on the instances
33dog.makeSound(); // Output: Woof Woof
34dog.move(); // Output: The animal moves.
35
36cat.makeSound(); // Output: Meow Meow
37cat.move(); // Output: The animal moves.
Functions
Function types and signatures
In TypeScript, you can define function types and signatures to specify the types of parameters and the return type of a function. This helps in ensuring type safety and provides better code readability and maintenance.1// Define a function type for adding numbers
2let add: (a: number, b: number) => number;
3
4// Implement the add function
5add = (x: number, y: number): number => {
6 return x + y;
7};
8
9console.log(add(2, 3)); // Output: 5
10
11// Define a function type for greeting
12type GreetFunction = (name: string) => string;
13
14// Function that takes another function as a parameter
15function greetUser(greet: GreetFunction, userName: string): void {
16 console.log(greet(userName));
17}
18
19// Implement the greet function
20const sayHello: GreetFunction = (name: string) => `Hello, ${name}!`;
21
22// Call the greetUser function
23greetUser(sayHello, "Alice"); // Output: Hello, Alice!
Optional and default parameters
TypeScript allows you to define functions with optional and default parameters. Optional parameters can be omitted when calling the function, while default parameters provide a default value if no value is provided.1// Function with optional parameter
2function greet(name: string, greeting?: string): string {
3 if (greeting) {
4 return `${greeting}, ${name}!`;
5 }
6 return `Hello, ${name}!`;
7}
8
9console.log(greet("Alice")); // Output: Hello, Alice!
10console.log(greet("Alice", "Hi")); // Output: Hi, Alice!
11
12// Function with default parameter
13function greetWithDefault(name: string, greeting: string = "Hello"): string {
14 return `${greeting}, ${name}!`;
15}
16
17console.log(greetWithDefault("Bob")); // Output: Hello, Bob!
18console.log(greetWithDefault("Bob", "Hi")); // Output: Hi, Bob!
19
20// Function with both optional and default parameters
21function createMessage(message: string, sender?: string, timestamp: Date = new Date()): string {
22 let fullMessage = `[${timestamp.toISOString()}] ${message}`;
23 if (sender) {
24 fullMessage += ` ${sender}`;
25 }
26 return fullMessage;
27}
28
29console.log(createMessage("Hello World")); // Output: [current date-time] Hello World
30console.log(createMessage("Hello World", "Admin")); // Output: [current date-time] Hello World - Admin
31console.log(createMessage("Hello World", "Admin", new Date(2020, 1, 1))); // Output: [2020-02-01T00:00:00.000Z] Hello World - Admin
Arrow functions
Arrow functions in TypeScript, as in JavaScript, provide a concise way to write functions. They are often used for simple functions and callbacks, and they have some differences from regular function expressions, particularly in how they handle the this context.1// Regular function
2function add(x: number, y: number): number {
3 return x + y;
4}
5
6// Arrow function
7const addArrow = (x: number, y: number): number => x + y;
8
9// Using the arrow function
10console.log(addArrow(5, 3)); // Output: 8
11
12// Arrow function with no parameters
13const sayHello = (): string => "Hello, TypeScript!";
14console.log(sayHello()); // Output: Hello, TypeScript!
15
16// Arrow function with a single parameter
17const square = (x: number): number => x * x;
18console.log(square(4)); // Output: 16
19
20// Arrow function with a block body
21const multiply = (x: number, y: number): number => {
22 const result = x * y;
23 return result;
24};
25console.log(multiply(4, 5)); // Output: 20
26
27// Arrow function in a class method
28class MathOperations {
29 add = (a: number, b: number): number => a + b;
30}
31
32const mathOps = new MathOperations();
33console.log(mathOps.add(10, 15)); // Output: 25
34
35// Arrow function handling `this` context
36class Counter {
37 count: number = 0;
38
39 increment = (): void => {
40 this.count++;
41 console.log(this.count);
42 }
43}
44
45const counter = new Counter();
46counter.increment(); // Output: 1
47counter.increment(); // Output: 2
Function overloading
Function overloading in TypeScript allows you to define multiple signatures for a single function. This means you can create a function that can handle different types of input or return different types of output depending on the arguments provided.1// Overload signatures
2function compute(value: number): number;
3function compute(value: string): string;
4function compute(value: number | string): number | string {
5 if (typeof value === "number") {
6 return value * value; // Return the square of the number
7 } else {
8 return value.toUpperCase(); // Return the uppercase of the string
9 }
10}
11
12// Using the function
13console.log(compute(4)); // Output: 16
14console.log(compute("hello")); // Output: HELLO
Generics
Generics in TypeScript allow you to write functions and classes that can work with different types while still providing type safety. Generics enable you to create components that are reusable and flexible, without losing the benefits of type checking.Generic functions and classes
A generic function allows you to specify a placeholder for the type of input or output. This placeholder is replaced with an actual type when the function is used.1// Define a generic function
2function identity<T>(value: T): T {
3 return value;
4}
5
6// Using the generic function
7const num = identity(5); // T is inferred as number
8const str = identity("hello"); // T is inferred as string
9const bool = identity(true); // T is inferred as boolean
10
11console.log(num); // Output: 5
12console.log(str); // Output: hello
13console.log(bool); // Output: true
1// Define a generic class
2class Box<T> {
3 private value: T;
4
5 constructor(value: T) {
6 this.value = value;
7 }
8
9 getValue(): T {
10 return this.value;
11 }
12
13 setValue(newValue: T): void {
14 this.value = newValue;
15 }
16}
17
18// Using the generic class
19const numberBox = new Box<number>(123);
20const stringBox = new Box<string>("hello");
21
22console.log(numberBox.getValue()); // Output: 123
23console.log(stringBox.getValue()); // Output: hello
24
25numberBox.setValue(456);
26stringBox.setValue("world");
27
28console.log(numberBox.getValue()); // Output: 456
29console.log(stringBox.getValue()); // Output: world
Generic constraints
Generic constraints in TypeScript allow you to restrict the types that can be used with generics, ensuring that they meet certain criteria or have specific properties. This feature helps enforce a contract on the types used in generic functions or classes.1// Define interfaces for constraints
2interface HasId {
3 id: number;
4}
5
6interface HasName {
7 name: string;
8}
9
10// Define a generic function with multiple constraints
11function printDetails<T extends HasId & HasName>(item: T): void {
12 console.log(`ID: ${item.id}, Name: ${item.name}`);
13}
14
15// Using the generic function
16printDetails({ id: 1, name: 'Alice' }); // Output: ID: 1, Name: Alice
17printDetails({ id: 2, name: 'Bob' }); // Output: ID: 2, Name: Bob
18
19// This will cause an error because the object does not have a name property
20// printDetails({ id: 3 }); // Error: Argument of type '{ id: number; }' is not assignable to parameter of type 'HasId & HasName'.
1// Define a generic function with a constraint
2function executeCallback<T extends Function>(callback: T): void {
3 callback();
4}
5
6// Using the generic function
7executeCallback(() => console.log('Callback executed!')); // Output: Callback executed!
8
9// This will cause an error because the argument is not a function
10// executeCallback('not a function'); // Error: Argument of type 'string' is not assignable to parameter of type 'Function'.
Using multiple type parameters
Using multiple type parameters in TypeScript allows you to create more flexible and reusable code by defining functions, classes, and interfaces that can work with different types. Here’s a comprehensive guide with examples:1// Define a generic class with multiple type parameters
2class Pair<T, U> {
3 constructor(public first: T, public second: U) {}
4
5 getFirst(): T {
6 return this.first;
7 }
8
9 getSecond(): U {
10 return this.second;
11 }
12}
13
14// Using the generic class
15const pair = new Pair<number, string>(1, 'hello');
16
17console.log(pair.getFirst()); // Output: 1
18console.log(pair.getSecond()); // Output: hello
Generic interfaces
Generic interfaces in TypeScript allow you to define interfaces that work with various data types. They enable you to create flexible and reusable structures by using type parameters. Here’s a detailed look at generic interfaces:1// Define a constraint interface
2interface HasName {
3 name: string;
4}
5
6// Define a generic interface with a constraint
7interface Person<T extends HasName> {
8 personInfo: T;
9}
10
11// Using the generic interface
12const person: Person<{ name: string; age: number }> = {
13 personInfo: { name: 'Bob', age: 25 }
14};
15
16console.log(person.personInfo); // Output: { name: 'Bob', age: 25 }
Modules
Importing and exporting modules
In TypeScript, importing and exporting modules allows you to manage and reuse code across different files. This is similar to how modules work in JavaScript. Here’s a comprehensive overview of how to import and export modules in TypeScript.1// Named exports
2export const PI = 3.14;
3
4export function add(x: number, y: number): number {
5 return x + y;
6}
7
8export class Calculator {
9 multiply(x: number, y: number): number {
10 return x * y;
11 }
12}
1// Importing named exports
2import { PI, add, Calculator } from './math';
3
4console.log(PI); // Output: 3.14
5console.log(add(2, 3)); // Output: 5
6
7const calc = new Calculator();
8console.log(calc.multiply(2, 3)); // Output: 6
Default and named exports
1// Default export
2export default function greet(name: string): string {
3 return `Hello, ${name}!`;
4}
1// Importing default export
2import greet from './utils';
3
4console.log(greet('TypeScript')); // Output: Hello, TypeScript!
Type Inference and Type Assertions
Understanding type inference
Type inference in TypeScript is a powerful feature that allows the TypeScript compiler to automatically determine the type of a variable or expression based on its usage. This can simplify code by reducing the need to explicitly specify types while still providing the benefits of static type checking.1let x = 5; // TypeScript infers that x is of type number
2let name = "Alice"; // TypeScript infers that name is of type string
3
4function add(a: number, b: number) {
5 return a + b; // TypeScript infers the return type to be number
6}
7
8function greet(message: string) {
9 console.log(message); // TypeScript infers that message is of type string
10}
11
12let numbers: number[] = [1, 2, 3]; // TypeScript infers that numbers is an array of numbers
13numbers.forEach(num => {
14 console.log(num); // TypeScript infers num to be of type number
15});
16
17
18const person = {
19 name: "Alice",
20 age: 30
21}; // TypeScript infers the type as { name: string; age: number }
22
23
24let numbers = [1, 2, 3]; // TypeScript infers numbers to be number[]
25let tuple: [string, number] = ["Alice", 30]; // TypeScript infers tuple to be [string, number]
26
27
28
29function combine(a: string, b: string): string;
30function combine(a: number, b: number): number;
31function combine(a: any, b: any): any {
32 return a + b;
33}
34
35function identity<T>(value: T): T {
36 return value;
37}
38
39let result = identity("Hello"); // TypeScript infers result to be string
40
41let someValue: any = "This is a string";
42let strLength: number = (someValue as string).length; // Using type assertion to specify the type
Using type assertions
In TypeScript, type assertions are a way to tell the compiler about the type of a variable when it cannot infer it automatically. This is useful when you know more about the type of a variable than TypeScript does. Type assertions provide a way to override the inferred type.1let someValue: any = "This is a string";
2let strLength: number = (someValue as string).length;
3console.log(strLength); // Output: 16
4
5
6let element = document.getElementById("myElement") as HTMLDivElement;
7element.style.backgroundColor = "blue";
8
9
10interface User {
11 name: string;
12 age: number;
13}
14
15let userData: any = {
16 name: "Alice",
17 age: 30
18};
19
20let user = userData as User;
21console.log(user.name); // Output: Alice
22
23
24
25let unknownValue: unknown = "A string";
26let stringValue: string = unknownValue as string;
Non-null assertion operator
The non-null assertion operator in TypeScript is a feature that allows you to tell the compiler that a value is not null or undefined when TypeScript's type inference cannot guarantee it. This operator can be useful in scenarios where you are sure that a value will not be null or undefined, even though TypeScript might not be able to verify it.1function getLength(value: string | null): number {
2 // Asserting that value is not null
3 return value!.length;
4}
5
6
7const element = document.getElementById("myElement");
8element!.style.backgroundColor = "blue";
9
10
11const person: { name?: string } = {};
12 // Asserting that person.name is not undefined
13console.log(person.name!.length);
Decorators
What are decorators?
Decorators in TypeScript are a powerful feature that allows you to add metadata or modify the behavior of classes, methods, accessors, properties, or parameters. They are a form of declarative programming that can be used to implement design patterns, such as Singleton or Observer, and add functionalities like logging, validation, or dependency injection.Decorator types
1//class decorator
2function Logger(constructor: Function) {
3 console.log('Class created:', constructor);
4}
5
6@Logger
7class MyClass {
8 constructor() {
9 console.log('MyClass instance created');
10 }
11}
12
13
14//Method decorator
15
16function Log(target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
17 const originalMethod = descriptor.value;
18
19 descriptor.value = function (...args: any[]) {
20 console.log(`Method ${String(propertyKey)} called with args: ${args}`);
21 return originalMethod.apply(this, args);
22 };
23}
24
25class MyClass {
26 @Log
27 greet(name: string) {
28 console.log(`Hello, ${name}!`);
29 }
30}
31
32const myClassInstance = new MyClass();
33myClassInstance.greet('Alice'); // Logs: Method greet called with args: [ 'Alice' ]
34
35
36
37
38//access decorators
39
40function ReadOnly(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
41 descriptor.writable = false;
42}
43
44class MyClass {
45 private _name: string = 'Default';
46
47 @ReadOnly
48 get name() {
49 return this._name;
50 }
51}
52
53const myClassInstance = new MyClass();
54console.log(myClassInstance.name); // Logs: Default
55// myClassInstance.name = 'NewName'; // Error: Cannot assign to 'name' because it is a read-only property.
56
57
58//property decorators
59function Required(target: any, propertyKey: string) {
60 const value = target[propertyKey];
61 if (value === undefined || value === null) {
62 throw new Error(`${String(propertyKey)} is required`);
63 }
64}
65
66class MyClass {
67 @Required
68 public name: string;
69
70 constructor(name: string) {
71 this.name = name;
72 }
73}
74
75// The following will throw an error as the 'name' property is required
76// const myClassInstance = new MyClass(undefined);
77
78
79
80//parameter decorator
81
82function LogParameter(target: any, propertyKey: string | symbol, parameterIndex: number) {
83 console.log(`Parameter at index ${parameterIndex} in method ${String(propertyKey)} is decorated.`);
84}
85
86class MyClass {
87 greet(@LogParameter name: string) {
88 console.log(`Hello, ${name}!`);
89 }
90}
Decorator Factories
1function Log(level: string) {
2 return function (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
3 const originalMethod = descriptor.value;
4
5 descriptor.value = function (...args: any[]) {
6 console.log(`[${level}] Method ${String(propertyKey)} called with args: ${args}`);
7 return originalMethod.apply(this, args);
8 };
9 };
10}
11
12class MyClass {
13 @Log('INFO')
14 greet(name: string) {
15 console.log(`Hello, ${name}!`);
16 }
17}
Advanced Types
Union and intersection types
Union types allow a value to be one of several types. You define a union type using the | operator. It’s useful when a variable can hold multiple types but you want to be explicit about what those types could be.1// A variable that can be either a string or a number
2let value: string | number;
3
4value = "Hello"; // Valid
5value = 42; // Valid
6value = true; // Error: Type 'boolean' is not assignable to type 'string | number'.
7
8
9function formatValue(value: string | number): string {
10 if (typeof value === 'string') {
11 return value.toUpperCase();
12 } else {
13 return value.toFixed(2);
14 }
15}
16
17console.log(formatValue("hello")); // Outputs: HELLO
18console.log(formatValue(123.456)); // Outputs: 123.46
19
20
21
22type SuccessResponse = {
23 success: true;
24 data: any;
25};
26
27type ErrorResponse = {
28 success: false;
29 error: string;
30};
31
32type ApiResponse = SuccessResponse | ErrorResponse;
33
34function handleResponse(response: ApiResponse) {
35 if (response.success) {
36 console.log("Data:", response.data);
37 } else {
38 console.log("Error:", response.error);
39 }
40}
1// Define two types
2type Person = {
3 name: string;
4 age: number;
5};
6
7type Contact = {
8 email: string;
9 phone: string;
10};
11
12// Create an intersection type
13type PersonContact = Person & Contact;
14
15const person: PersonContact = {
16 name: "Alice",
17 age: 30,
18 email: "[email protected]",
19 phone: "123-456-7890",
20};
21
22
23
24type User = {
25 id: number;
26 username: string;
27};
28
29type Admin = {
30 role: string;
31};
32
33type AdminUser = User & Admin;
34
35function getAdminInfo(user: AdminUser) {
36 console.log(`ID: ${user.id}`);
37 console.log(`Username: ${user.username}`);
38 console.log(`Role: ${user.role}`);
39}
40
41const admin: AdminUser = {
42 id: 1,
43 username: "adminUser",
44 role: "Administrator",
45};
46
47getAdminInfo(admin);
1type Car = {
2 make: string;
3 model: string;
4};
5
6type Motorcycle = {
7 make: string;
8 engineCapacity: number;
9};
10
11type Vehicle = Car | Motorcycle; // Vehicle can be either a Car or a Motorcycle
12
13type DetailedVehicle = Vehicle & {
14 registrationNumber: string;
15};
16
17const vehicle: DetailedVehicle = {
18 make: "Honda",
19 model: "Civic",
20 registrationNumber: "ABC123",
21};
22
23const bike: DetailedVehicle = {
24 make: "Yamaha",
25 engineCapacity: 600,
26 registrationNumber: "XYZ789",
27};
Type guards and type predicates
Type guards
Type guards are mechanisms that allow TypeScript to infer a more specific type within a conditional block. They are typically implemented using conditional statements and can be used with several patterns.1function printLength(value: string | number) {
2 if (typeof value === 'string') {
3 console.log(`String length: ${value.length}`);
4 } else {
5 console.log(`Number value: ${value}`);
6 }
7}
8
9printLength("Hello"); // String length: 5
10printLength(123); // Number value: 123
11
12
13
14class Dog {
15 bark() {
16 console.log("Woof!");
17 }
18}
19
20class Cat {
21 meow() {
22 console.log("Meow!");
23 }
24}
25
26function makeSound(animal: Dog | Cat) {
27 if (animal instanceof Dog) {
28 animal.bark();
29 } else if (animal instanceof Cat) {
30 animal.meow();
31 }
32}
33
34makeSound(new Dog()); // Woof!
35makeSound(new Cat()); // Meow!
36
37
38
39interface Admin {
40 admin: true;
41 role: string;
42}
43
44interface User {
45 admin?: false;
46 username: string;
47}
48
49function getRole(user: Admin | User) {
50 if ('role' in user) {
51 console.log(`Role: ${user.role}`);
52 } else {
53 console.log(`Username: ${user.username}`);
54 }
55}
56
57getRole({ admin: true, role: "Manager" }); // Role: Manager
58getRole({ username: "JohnDoe" }); // Username: JohnDoe
Type predicates
Type predicates are a special kind of type guard that explicitly informs TypeScript about the type of a variable. They are functions that return a boolean and include a type predicate in the return type. This helps TypeScript to infer the type of a variable in a conditional block.1function isString(value: any): value is string {
2 return typeof value === 'string';
3}
4
5function printLength(value: any) {
6 if (isString(value)) {
7 console.log(`String length: ${value.length}`);
8 } else {
9 console.log(`Not a string`);
10 }
11}
12
13printLength("Hello"); // String length: 5
14printLength(123); // Not a string
15
16
17
18//Type Predicates with User-defined Types
19
20interface Admin {
21 admin: true;
22 role: string;
23}
24
25interface User {
26 admin?: false;
27 username: string;
28}
29
30function isAdmin(user: Admin | User): user is Admin {
31 return (user as Admin).admin === true;
32}
33
34function getRole(user: Admin | User) {
35 if (isAdmin(user)) {
36 console.log(`Role: ${user.role}`);
37 } else {
38 console.log(`Username: ${user.username}`);
39 }
40}
41
42getRole({ admin: true, role: "Manager" }); // Role: Manager
43getRole({ username: "JohnDoe" }); // Username: JohnDoe
Mapped types
Mapped Types in TypeScript allow you to create new types by transforming properties of an existing type. They are particularly useful when you want to derive new types based on existing ones, applying transformations or constraints to the properties.1//1. Read-Only Properties
2
3type ReadOnly<T> = {
4 readonly [P in keyof T]: T[P];
5};
6
7interface Person {
8 name: string;
9 age: number;
10}
11
12type ReadOnlyPerson = ReadOnly<Person>;
13
14// Usage
15const person: ReadOnlyPerson = {
16 name: "Alice",
17 age: 30
18};
19
20// person.name = "Bob"; // Error: cannot assign to 'name' because it is a read-only property
21
22
23
24
25//partial properties
26type Partial<T> = {
27 [P in keyof T]?: T[P];
28};
29
30interface Person {
31 name: string;
32 age: number;
33}
34
35type PartialPerson = Partial<Person>;
36
37// Usage
38const person: PartialPerson = {
39 name: "Alice"
40}; // Age is optional
41
42
43
44
45//required properties
46type Required<T> = {
47 [P in keyof T]-?: T[P];
48};
49
50interface Person {
51 name?: string;
52 age?: number;
53}
54
55type RequiredPerson = Required<Person>;
56
57// Usage
58const person: RequiredPerson = {
59 name: "Alice",
60 age: 30
61}; // Both properties are required
62
63
64
65
66
67//Mapping to new types
68
69type ToString<T> = {
70 [P in keyof T]: string;
71};
72
73interface Person {
74 name: string;
75 age: number;
76}
77
78type PersonToString = ToString<Person>;
79
80// Usage
81const person: PersonToString = {
82 name: "Alice",
83 age: "30" // Age is now a string
84};
85
86
87
88
89//Modifying property types
90
91type MakeNullable<T> = {
92 [P in keyof T]: T[P] | null;
93};
94
95interface Person {
96 name: string;
97 age: number;
98}
99
100type NullablePerson = MakeNullable<Person>;
101
102// Usage
103const person: NullablePerson = {
104 name: null,
105 age: 30
106}; // Name can be null
Conditional types
Conditional Types in TypeScript provide a way to create types that depend on a condition. They are similar to conditional statements in regular programming but operate at the type level. Conditional types can be used to create type transformations based on the type of inputs.1type IsString<T> = T extends string ? "Yes" : "No";
2
3type Result1 = IsString<string>; // "Yes"
4type Result2 = IsString<number>; // "No"
5
6
7//Conditional Types with Generic Types
8
9type ExtractType<T> = T extends Array<infer U> ? U : T;
10
11type NumberArray = ExtractType<number[]>; // number
12type StringType = ExtractType<string>; // string
13
14
15//Conditional Types with Default Types
16type DefaultValue<T = string> = T extends undefined ? "Default" : T;
17
18type Result1 = DefaultValue<number>; // number
19type Result2 = DefaultValue<undefined>; // "Default"
20type Result3 = DefaultValue; // string (default)
21
22
23//Conditional Types with Multiple Types
24type ComplexType<T> = T extends (infer U)[]
25 ? U extends number
26 ? "Number Array"
27 : "Other Array"
28 : "Not an Array";
29
30type Result1 = ComplexType<number[]>; // "Number Array"
31type Result2 = ComplexType<string[]>; // "Other Array"
32type Result3 = ComplexType<number>; // "Not an Array"
33
34
35//Conditional Types with Type Guards
36type IsArray<T> = T extends any[] ? true : false;
37
38type Test1 = IsArray<number[]>; // true
39type Test2 = IsArray<number>; // false
40
41
42//Distributive Conditional Types
43type IsString<T> = T extends string ? "Yes" : "No";
44
45type Result = IsString<string | number>; // "Yes" | "No"
Utility types
TypeScript provides several built-in utility types to help with common type transformations and operations. These utility types simplify type manipulation, making it easier to work with complex types and perform type transformations.1//Partial
2interface User {
3 name: string;
4 age: number;
5}
6
7const updateUser = (user: Partial<User>) => {
8 // Implementation to update user
9};
10
11updateUser({ name: "Alice" }); // Valid
12updateUser({}); // Valid
13
14//Required
15interface User {
16 name?: string;
17 age?: number;
18}
19
20const user: Required<User> = { name: "Alice", age: 30 }; // Valid
21const invalidUser: Required<User> = { name: "Bob" }; // Error: Property 'age' is missing
22
23
24//Readonly
25interface User {
26 name: string;
27 age: number;
28}
29
30const user: Readonly<User> = { name: "Alice", age: 30 };
31user.name = "Bob"; // Error: Cannot assign to 'name' because it is a read-only property
32
33//Record
34type UserRoles = "admin" | "user" | "guest";
35const roleDescriptions: Record<UserRoles, string> = {
36 admin: "Administrator",
37 user: "Regular user",
38 guest: "Guest user",
39};
40
41
42//Pick
43type UserRoles = "admin" | "user" | "guest";
44const roleDescriptions: Record<UserRoles, string> = {
45 admin: "Administrator",
46 user: "Regular user",
47 guest: "Guest user",
48};
49
50
51//Omit
52interface User {
53 name: string;
54 age: number;
55 email: string;
56}
57
58type UserWithoutEmail = Omit<User, "email">;
59
60const user: UserWithoutEmail = { name: "Alice", age: 30 }; // Valid
61
62// Error: Property 'email' does not exist
63const invalidUser: UserWithoutEmail = { name: "Bob", email: "[email protected]" };
64
65
66//Exclude
67type AllPossible = "a" | "b" | "c";
68type SomeExcluded = Exclude<AllPossible, "b" | "c">; // "a"
69
70//Extract
71type AllPossible = "a" | "b" | "c";
72type SomeExtracted = Extract<AllPossible, "b" | "c">; // "b" | "c"
73
74
75//NonNullable
76type NullableString = string | null | undefined;
77type NonNullableString = NonNullable<NullableString>; // string
78
79
80//Parameters
81type FunctionParams = Parameters<(a: number, b: string) => void>; // [number, string]
82
83
84//ReturnType
85type FunctionReturn = ReturnType<() => number>; // number
86
87
88//InstanceType
89class User {
90 constructor(public name: string) {}
91}
92
93type UserInstance = InstanceType<typeof User>; // User