Notes
TypeScript is a powerful, statically typed superset of JavaScript designed to help developers write safer, more maintainable, and error-free code. It enhances JavaScript by adding syntax for types, focusing on type safety. This means many issues can be caught during development, before the code even runs.
Why TypeScript?
-
Type Safety: TypeScript’s core benefit is static typing, which means variable types are checked at compile-time.
- This contrasts with dynamically typed languages like JavaScript, where types are determined at runtime and can change, potentially leading to unexpected errors.
- TypeScript ensures that a variable declared as a number will only store number values, preventing errors like trying to add a number to a string and getting a concatenated string result (e.g.,
2 + "2"becoming"22").
-
Maintainability and Robustness: By catching type-related errors early, TypeScript makes code more robust and easier to maintain, especially in large projects and collaborative environments.
-
Readability and Clarity: Explicit type annotations improve code readability, making it clearer to both developers and the TypeScript compiler what types of values variables and functions are expected to handle.
-
Industry Demand: TypeScript is a valuable skill in the job market due to its high industry demand.
-
Efficiency and Error Prevention: It reduces the need for extensive unit testing, saving development time, and provides instant feedback on errors before runtime, leading to faster and more reliable development.
-
Seamless Integration: TypeScript integrates seamlessly with existing JavaScript code, making adoption painless.
-
Framework Empowerment: It enhances popular frameworks like React, Vue, and Angular with advanced features such as interfaces.
How TypeScript Works: Transpilation
TypeScript code (.ts files) is not directly executed by browsers or Node.js. Instead, it is transpiled (or compiled) into plain JavaScript code (.js files) by the TypeScript compiler (TSC). This JavaScript output can then be run in any JavaScript environment.
graph LR A["TypeScript Code (.ts)"] --> B["TypeScript Compiler (TSC)"] B --> C["JavaScript Code (.js)"] C --> D["JavaScript Runtime (Browser, Node.js)"]
Flowchart: TypeScript Compilation Process
Setting Up Your TypeScript Environment
To work with TypeScript, you’ll need:
-
Git: A version control system.
-
Visual Studio Code (VS Code): A popular code editor with excellent TypeScript support.
-
Node.js: Includes
npm(Node Package Manager), which is used to install TypeScript.- Verify Node.js Installation: Open your terminal and run
node -v. It should display the installed Node.js version.
- Verify Node.js Installation: Open your terminal and run
-
TypeScript: Install TypeScript globally using npm.
-
Install Command:
npm install -g typescript -
Verify TypeScript Installation: After installation, run
tsc -v. It should display the installed TypeScript version.
-
Your First TypeScript Application
-
Create a folder: For example,
TypeScripton your desktop. -
Create a file: Inside the
TypeScriptfolder, createindex.tsand open it in VS Code. -
Write code:
console.log(Math.floor());You’ll immediately notice VS Code highlighting
Math.flooras an error, indicating “Expected 1 argument, but got 0”. This is TypeScript’s type checking in action, catching errors before you run the code. -
Fix the error: Add a number argument, e.g.,
11.3.console.log(Math.floor(11.3)); -
Compile TypeScript to JavaScript:
-
Open your terminal and navigate to your
TypeScriptfolder. -
Run
tsc index.ts. This will generate anindex.jsfile in the same directory.
-
-
Run the JavaScript file:
- In the terminal, run
node index.js. You should see the output in the console.
- In the terminal, run
Project Configuration with tsconfig.json
For larger projects, it’s beneficial to configure the TypeScript compiler using a tsconfig.json file.
-
Structure your project: Create a
srcfolder and moveindex.tsinto it. -
Generate
tsconfig.json: In your project’s root directory, runtsc --init. This command creates atsconfig.jsonfile with default compiler options. -
Customize
tsconfig.json: Open thetsconfig.jsonfile. Two important options are:-
rootDir: Defines the root directory for your TypeScript source files. Set it to"./src". -
outDir: Specifies the output directory for compiled JavaScript files. Set it to"./dist"(orbestas in the source). -
Save your changes.
-
-
Compile with
tsconfig.json: From your project root, runtsc. TypeScript will now use the settings defined intsconfig.jsonto compile your code. This will generateindex.jsinside thedist(orbest) folder. -
Run compiled code:
node dist/index.js(ornode best/index.js).
graph TD A[Project Root] --> B[src/index.ts] A --> C[tsconfig.json] C -- rootDir: ./src --> B C -- outDir: ./dist --> D[dist/index.js] B -- tsc command --> D
Flowchart: Project Structure with tsconfig.json
Basic Types in TypeScript
TypeScript extends JavaScript’s built-in types (numbers, strings, booleans, null, undefined, objects) with additional types for enhanced safety and expressiveness.
-
JavaScript Built-in Types:
-
number -
string -
boolean -
null -
undefined -
object
-
-
TypeScript Introduced Types:
-
any: Represents any type of value. It disables type checking for that variable. Avoid usinganywhenever possible as it negates type safety, reduces code readability, and makes it less maintainable. -
unknown: A safer alternative toany. -
never: Represents values that never occur, often associated with functions that throw exceptions or enter infinite loops. -
enum: Defines a set of named constant values. -
tuple: Similar to arrays but with a fixed number of elements and specific types for each position.
-
Type Annotation
Type annotations explicitly specify the type of a variable, function, or other entity using a colon (:) followed by the type. This helps the TypeScript compiler enforce intended usage.
-
With Variables:
let myNumber: number = 10; // Explicitly annotated as number let myString: string = "Hello"; // Explicitly annotated as stringEven if you declare a variable and assign a value later, TypeScript understands the type based on the initial declaration.
-
Dynamic Type Determination (Type Inference): If you don’t use type annotations, TypeScript can often infer the type based on the assigned value.
let greeting = "Hello, TypeScript!"; // TypeScript infers 'greeting' as stringWhile convenient, it’s generally best practice to use type annotations for clarity and explicit type safety, as relying solely on inference can sometimes lead to unexpected issues.
Type Annotation with Objects
Type annotations with objects allow you to define the types of properties an object should have.
type Address = {
street: string;
city: string;
};
type Person = {
name: string;
age: number;
jobTitle?: string; // Optional property
address: Address; // Nested object with type annotation
};
let personExample2: Person = {
name: "Alice",
age: 30,
address: {
street: "123 Main St",
city: "Anytown"
}
};
// 'jobTitle' is optional and can be omittedType Annotation with Functions
Type annotations for functions explicitly define the data types for parameters and return values.
function calculateRectangleArea(length: number, width: number): number { // Parameters and return type annotated
return length * width; //
}
let area: number = calculateRectangleArea(10, 5); //
console.log(area);Optional and Default Parameters
Functions can have optional parameters (marked with ?) or parameters with default values (using =).
// Optional parameter
function greet(name: string, greeting?: string): void {
if (greeting) {
console.log(`${greeting}, ${name}!`);
} else {
console.log(`Hello, ${name}!`);
}
}
greet("Alice"); // Hello, Alice!
greet("Bob", "Hi"); // Hi, Bob!
// Default parameter
function sayHello(name: string = "Guest"): void {
console.log(`Hello, ${name}!`);
}
sayHello(); // Hello, Guest!
sayHello("Charlie"); // Hello, Charlie!Rest Parameters
Rest parameters allow a function to accept an indefinite number of arguments as an array, using the spread operator (...).
function addAll(...nums: number[]): number { // nums will be an array of numbers
let sum = 0;
for (let i = 0; i < nums.length; i++) { //
sum += nums[i];
}
return sum;
}
console.log(addAll(1, 2, 3)); // 6
console.log(addAll(10, 20, 30, 40)); // 100Arrow Functions and Anonymous Functions
-
Arrow Functions: Provide a more compact syntax for defining functions.
const add = (num1: number, num2: number): number => num1 + num2; // -
Anonymous Functions: Functions without a name, often defined as an expression. Useful for small, one-off functions to avoid polluting the global scope.
let subtract = function (a: number, b: number): number { return a - b; };
void and never Types
-
void: Indicates the absence of a value. Used for functions that don’t return any value or returnundefined(e.g., functions that perform actions or side effects).function logMessage(message: string): void { console.log(message); } -
never: Represents values that never occur. Typically associated with functions that throw exceptions, enter infinite loops, or have unreachable code.function throwError(message: string): never { // throw new Error(message); } function infiniteLoop(): never { // while (true) { // ... } }
Union Types
Union types allow a variable to hold values of multiple specified types, denoted by a pipe (|) symbol.
let myVar: string | number; // Can be a string or a number
myVar = "hello"; // Valid
myVar = 123; // Valid
// myVar = true; // Error: Type 'boolean' is not assignable to type 'string | number'.Literal Types
Literal types allow you to specify exact values that are allowed for a variable or parameter, rather than just a general type.
type Direction = "left" | "right" | "up" | "down"; //
let playerDirection: Direction = "left"; // Valid
// playerDirection = "forward"; // Error: Type '"forward"' is not assignable to type 'Direction'.
function setColor(color: "red" | "green" | "blue"): void { //
console.log(`Selected color: ${color}`);
}
setColor("red"); // Valid
// setColor("yellow"); // ErrorLiteral types enhance type safety by restricting input to a predefined set of values, catching errors at compile-time.
Nullable Types
Nullable types allow a variable or parameter to hold either a specific data type (like string or number) or the value null. This is useful for scenarios where a value might be absent or undefined. You create them by appending | null to an existing type.
let username: string | null = "John Doe"; // Can be string or null
username = null; // Valid
let age: number | null = 25; // Can be number or null
age = null; // Valid
function displayUsername(name: string | null): void { //
if (name === null) {
console.log("Welcome, Guest!"); //
} else {
console.log(`Welcome, ${name}!`);
}
}
displayUsername("Alice");
displayUsername(null);Nullable types improve code safety and clarity by explicitly indicating the possibility of missing data.
Type Aliases
Type aliases allow you to create custom names for types, improving readability, maintainability, and reusability for complex type definitions.
type MyString = string; // Alias for string type
type StringOrNumber = string | number; // Alias for a union type
let myName: MyString = "Bob"; // Using type alias
let myValue: StringOrNumber = 42; // Using type alias
// Type alias with objects
type Employee = {
name: string; // Required string
age: number; // Required number
email?: string; // Optional string
};
let alice: Employee = {
name: "Alice",
age: 30,
email: "alice@example.com"
}; //
let bob: Employee = {
name: "Bob",
age: 25
}; // Omits optional emailType aliases provide a way to define complex object structures and reuse them throughout your code.
Intersection Types
Intersection types combine multiple types into one, creating a new type that possesses all properties and functionalities of the individual types being intersected. They are denoted by the & operator.
type FirstType = {
prop1: string;
};
type SecondType = {
prop2: number;
};
type CombinedType = FirstType & SecondType; // Combines properties of both
let myCombinedObject: CombinedType = {
prop1: "hello",
prop2: 123
};Type Annotation with Arrays
Arrays in TypeScript can be type-annotated to specify the expected data type of their elements.
let fruits: string[] = ["apple", "banana", "orange"]; // Array of strings
for (let fruit of fruits) {
console.log(fruit.toUpperCase()); //
}
let numbers: number[] = [1, 2, 3]; // Array of numbers
let mixedArray: (string | number)[] = ["hello", 1, "world", 2]; // Array of strings or numbersTuples
A tuple is a data type similar to an array but with a fixed number of elements, where you can specify the type of each element at a specific position.
let article: [number, string, boolean] = [1, "TypeScript Tutorial", true]; //
// Reassigning a new tuple value that matches the type structure
article = [2, "Advanced TypeScript", false];
// Tuples have fixed size; cannot add additional elements
// article.push("new element"); // Error
// Destructuring to extract elements
const [id, title, published] = article;
console.log(`ID: ${id}, Title: ${title}, Published: ${published}`);Enums
Enums (enumerations) define a set of named constant values, providing a readable and expressive way to work with discrete options or categories. By default, enum members are assigned numeric values starting from 0.
enum Days {
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday
} //
let today: Days = Days.Wednesday; //
console.log(today); // Output: 3 (default numeric value)
console.log(Days[today]); // Output: Wednesday (retrieving name from value)
enum StatusCodes {
OK = 200,
BadRequest = 400,
NotFound = 404
}
let currentStatus: StatusCodes = StatusCodes.OK;
console.log(currentStatus); // Output: 200Enums improve code readability by providing human-readable names for specific values and are commonly used for categories like days of the week or status codes.
Interfaces
An interface in TypeScript defines a contract or blueprint for the shape and structure of an object. It specifies properties, methods, and their types that objects implementing the interface should have.
-
Shape Definition: Interfaces specify object structure, including property names, types, and optional/required status.
-
Contract: Objects or classes adhering to an interface must implement the defined structure and methods.
-
Type Checking: TypeScript checks if an object meets an interface’s requirements, catching type errors early.
-
Code Clarity: Interfaces document expected object properties and enhance code readability.
interface PersonExample1 {
name: string;
age: number;
} //
let alice: PersonExample1 = {
name: "Alice",
age: 30
}; //Interface Methods and Parameters
Interfaces can also define the signatures of functions or methods that an object adhering to the interface should have, including parameters and return types.
interface PersonExample2 {
name: string; // Property type string
age: number; // Property type number
greet(message: string): void; // Method signature: takes string, returns void
}
let aliceWithMethod: PersonExample2 = {
name: "Alice",
age: 30,
greet: function(message: string): void { //
console.log(`Hello ${this.name}, ${message}`); //
}
};
aliceWithMethod.greet("nice to meet you!"); // Method invocationInterface Reopening (Declaration Merging)
Interface reopening allows you to define multiple interfaces with the same name, and TypeScript will merge them into a single interface. This promotes modularity and code organization, allowing different parts of a project to contribute to an interface’s definition.
interface Settings {
theme: string;
font: string;
} // Initial interface
// Reopen the interface to add a property specific to an 'articles' page
interface Settings {
sidebar?: boolean; // Optional property
}
// Reopen the interface again for a 'contact' page
interface Settings {
externalLinks?: boolean;
}
let userSettings: Settings = {
theme: "dark",
font: "Arial",
sidebar: true,
externalLinks: false
}; // userSettings compiles with the merged interfaceBuilt-in Interfaces
TypeScript provides built-in interfaces for common JavaScript constructs and browser APIs, such as HTML elements. For example, HTMLElement, HTMLDivElement, and HTMLImageElement. These interfaces define properties and methods available for working with DOM elements, ensuring type safety when manipulating them.
Example: HTMLImageElement interface properties and methods:
-
Properties:
alt(string),height(number),src(string),width(number). -
Methods/Properties (status):
complete(boolean),decode(Promise),naturalHeight(number),naturalWidth(number).
Interface vs. Type Alias
| Feature | Interface | Type Alias |
|---|---|---|
| Declaration | interface keyword | type keyword |
| Extending/Implementing | Can be extended by other interfaces and implemented by classes | Cannot be directly extended or implemented, but similar results can be achieved with intersection types |
| Declaration Merging | Supports declaration merging (multiple interfaces with same name are merged) | Does not support declaration merging (multiple type aliases with same name cause an error) |
| Use Cases | Defining the shape of objects, especially for public APIs, and defining contracts for classes | Creating custom types that can be combined using union or intersection types, defining complex types, and giving descriptive names to combinations of types |
| Readability/Style | Often preferred for object shapes due to clear intent and better editor support (autocompletion, docs) | Useful for defining union, intersection, and other complex types |
In most cases, the choice depends on your specific use case and coding style. Both are valuable tools for defining custom types and enhancing code quality.
Classes in TypeScript
A class serves as a blueprint for creating objects with shared properties and methods, aiding in the organization and structure of your code. Class type annotations specify the types of properties, methods, and constructor parameters, enabling TypeScript to perform type checking and ensure instances adhere to the defined structure.
class Product {
id: number; // Property type annotation
name: string; // Property type annotation
price: number; // Property type annotation
constructor(id: number, name: string, price: number) { // Constructor parameter type annotation
this.id = id;
this.name = name;
this.price = price;
}
getProductInfo(): string { // Method return type annotation
return `Product ID: ${this.id}, Name: ${this.name}, Price: $${this.price}`;
}
}
let productOne = new Product(1, "Laptop", 1200); // Creating an instance
console.log(productOne.getProductInfo()); // Calling methodClass Access Modifiers
Access modifiers control the visibility and accessibility of class members (properties and methods) from outside the class.
-
public: Members are accessible from anywhere, both within and outside the class. This is the default modifier.class MyClass { public myPublicProperty: string = "I am public"; } let instance = new MyClass(); console.log(instance.myPublicProperty); // Accessible -
private: Members are only accessible from within the class where they are defined. They cannot be accessed from outside.class MyClass { private myPrivateProperty: string = "I am private"; public showPrivate() { console.log(this.myPrivateProperty); // Accessible inside } } let instance = new MyClass(); // console.log(instance.myPrivateProperty); // Error instance.showPrivate(); -
protected: Members are accessible from within the class they are defined in, and from subclasses (derived classes). They cannot be accessed from outside the class or unrelated classes.class Parent { protected protectedProperty: string = "I am protected"; } class Child extends Parent { public showProtected() { console.log(this.protectedProperty); // Accessible in child class } } let parentInstance = new Parent(); // console.log(parentInstance.protectedProperty); // Error (cannot access from outside) let childInstance = new Child(); childInstance.showProtected();
These modifiers help encapsulate internal details and control how class members are manipulated, crucial for code maintainability, integrity, and security.
Class Accessors (Getters and Setters)
Accessors (getters and setters) control access to class properties, allowing you to get and set values while providing additional logic or validation if needed. They are defined using the get and set keywords.
class ProductAccessors {
private _price: number; // Private property to store the actual price
constructor(price: number) {
this._price = price;
}
get price(): number { // Getter method to retrieve price
return this._price;
}
set price(newPrice: number) { // Setter method to set price with validation
if (newPrice < 0) {
console.log("Price cannot be negative."); //
return;
}
this._price = newPrice;
}
getProductInfo(): string {
return `Product Price: $${this.price}`; // Uses the getter
}
}
let productWithAccessors = new ProductAccessors(100);
console.log(productWithAccessors.price); // Accessing getter
productWithAccessors.price = 150; // Using setter
productWithAccessors.price = -20; // Setter logic prevents negative priceClass Static Members
Static members (properties or methods) belong to the class itself, not to instances of the class. They are accessed directly on the class name without creating an instance, using the static keyword.
class ProductStaticMembers {
private static nextId: number = 1; // Private static property to manage unique IDs
id: number;
name: string;
constructor(name: string) {
this.id = ProductStaticMembers.generateNextId(); // Using static method
this.name = name;
}
private static generateNextId(): number { // Static method to generate next ID
return ProductStaticMembers.nextId++;
}
getProductInfo(): string {
return `Product ID: ${this.id}, Name: ${this.name}`;
}
}
let product1 = new ProductStaticMembers("Keyboard");
let product2 = new ProductStaticMembers("Mouse");
console.log(product1.getProductInfo());
console.log(product2.getProductInfo());Class Implements Interface
A class can implement an interface, ensuring that it provides all the properties and methods required by that interface. This helps enforce a consistent structure for objects created from that class.
interface ProductImplementInterface { // Interface defining product structure
id: number;
name: string;
getProductInfo(): string;
}
class ProductClass implements ProductImplementInterface { // Class implementing the interface
id: number;
name: string;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
getProductInfo(): string { // Must implement this method as per interface
return `Product ID: ${this.id}, Name: ${this.name}`;
}
}
let productOne: ProductImplementInterface = new ProductClass(1, "Monitor"); // Assigning to interface type
let productTwo: ProductImplementInterface = new ProductClass(2, "Webcam");
console.log(productOne.getProductInfo());
console.log(productTwo.getProductInfo());Abstract Classes and Members
Abstract classes serve as blueprints for other classes. They cannot be instantiated on their own but can be subclassed. Abstract classes can contain abstract methods, which are declared but not implemented in the abstrac1t class itself. Subclasses are required to provide implementations for these abstract methods.
abstract class AbstractItem { // Abstract class
private static nextId: number = 1; // Private static property
id: number;
name: string;
constructor(name: string) {
this.id = AbstractItem.generateNextId();
this.name = name;
}
private static generateNextId(): number { // Static method
return AbstractItem.nextId++;
}
abstract getItemInfo(): string; // Abstract method, must be implemented by subclasses
}
class ConcreteItem extends AbstractItem { // Concrete class extending abstract class
constructor(name: string) {
super(name); // Call parent constructor
}
getItemInfo(): string { // Implementation of the abstract method
return `Item ID: ${this.id}, Name: ${this.name}`;
}
}
let itemOne = new ConcreteItem("Book"); //
let itemTwo = new ConcreteItem("Pen");
console.log(itemOne.getItemInfo()); //
console.log(itemTwo.getItemInfo());Polymorphism and Method Overriding
-
Polymorphism: Allows objects of different classes to be treated as objects of a common superclass.
-
Method Overriding: Occurs when a subclass provides a specific implementation of a method that is already defined in its superclass, allowing the subclass to customize the behavior.
abstract class AbstractEntity { // Abstract base class
private static nextId: number = 1;
protected name: string; // Protected to allow access in derived classes
id: number;
constructor(name: string) {
this.id = AbstractEntity.nextId++;
this.name = name;
}
abstract getEntityInfo(): string; // Abstract method
}
class Entity extends AbstractEntity { // Concrete subclass
constructor(name: string) {
super(name); // Call parent constructor
}
getEntityInfo(): string { // Overridden method
return `Entity ID: ${this.id}, Name: ${this.name}`;
}
}
class AnotherEntity extends AbstractEntity { // Another concrete subclass
constructor(name: string) {
super(name); // Call parent constructor
}
getEntityInfo(): string { // Overridden method
return `Another Entity ID: ${this.id}, Specific Name: ${this.name}`;
}
}
let entity1: AbstractEntity = new Entity("User"); // Polymorphism: stored as AbstractEntity type
let entity2: AbstractEntity = new AnotherEntity("Product"); // Polymorphism: stored as AbstractEntity type
console.log(entity1.getEntityInfo()); // Calls overridden method in Entity class
console.log(entity2.getEntityInfo()); // Calls overridden method in AnotherEntity classKey Difference: A class is for creating objects, while an interface is for defining the shape of an object.
Generics in TypeScript
Generics are a powerful feature that allows you to write reusable code by passing types as parameters to other types (classes, interfaces, or functions). This enables working flexibly with various types without resorting to the any type.
Advantages of Generics:
-
Code Reusability: Use the same code with different types without rewriting it.
-
Enhanced Type Safety: Helps detect potential errors at compile time rather than runtime.
-
Dealing with Multiple Types: Allows working with many types without specifying a particular one.
Generics can be used to create generic classes, functions, interfaces, and methods.
Generic Functions
function returnType<T>(value: T): T { // Generic function: takes type T, returns type T
return value;
}
console.log(returnType<number>(123)); // Explicitly specify type parameter
console.log(returnType<string>("hello"));
console.log(returnType<boolean>(true));
console.log(returnType<number[]>([1, 2, 3]));The function provides type safety, ensuring the return value has the same type as the input value.
Generics with Multiple Types
Generics can handle multiple types using union types or intersection types, creating flexible and versatile code.
// Generic arrow function
const returnTypeExample = <T>(value: T): T => value; //
console.log(returnTypeExample<number>(100));
console.log(returnTypeExample<string>("Test"));
// Generic function with multiple types
function multipleTypeFunction<T, S>(value1: T, value2: S): string { // Accepts two different types T and S
return `Value 1: ${value1} (Type: ${typeof value1}), Value 2: ${value2} (Type: ${typeof value2})`;
}
console.log(multipleTypeFunction<string, number>("Name", 30));
console.log(multipleTypeFunction<boolean, string>(true, "Status"));Generic Classes
Generic classes allow flexible and reusable class structures that work with a variety of data types, ensuring code flexibility and type safety.
class User<T = string> { // Generic class with default type parameter T = string
value: T;
constructor(initialValue: T) { // Constructor takes initial value of type T
this.value = initialValue;
}
show(message: T): void { // Method works with type T
console.log(`Message: ${message}, Value: ${this.value}`);
}
}
let userOne = new User<string>("Alice"); // User instance with string type parameter
userOne.show("Hello");
let userTwo = new User<number>(123); // User instance with number type parameter
userTwo.show(456);
let userThree = new User("Default String User"); // Uses default string type for T
userThree.show("Hi there!");Generics in Interfaces
Generics in interfaces allow creating reusable and type-safe data structures that work with different types, enhancing code flexibility and maintainability.
interface Book {
title: string;
author: string;
} //
interface Game {
name: string;
platform: string;
} //
interface Collection<T> { // Generic interface for a collection of type T
data: T[];
add(item: T): void;
}
class ItemCollection<T> implements Collection<T> { // Generic class implementing the generic interface
data: T[] = [];
add(item: T): void {
this.data.push(item);
}
}
let bookCollection: ItemCollection<Book> = new ItemCollection<Book>(); // Collection instance for Book type
bookCollection.add({ title: "The Hobbit", author: "J.R.R. Tolkien" });
let gameCollection: ItemCollection<Game> = new ItemCollection<Game>(); // Collection instance for Game type
gameCollection.add({ name: "Cyberpunk 2077", platform: "PC" });
console.log(bookCollection.data);
console.log(gameCollection.data);Type Assertions
Type assertions explicitly inform the TypeScript compiler about the expected type of a value, even if the compiler cannot automatically determine it. It’s like “telling” TypeScript to trust you about the type.
let someValue: any = "this is a string"; // 'any' type
// Using 'as' syntax for type assertion
let strLength: number = (someValue as string).length; // Asserting 'someValue' as string
console.log(strLength); // Output: 16
// Using angle-bracket syntax (less common with JSX)
let anotherValue: any = "hello world";
let upperCaseString: string = (<string>anotherValue).toUpperCase();
console.log(upperCaseString); // Output: HELLO WORLDCaution: Use type assertions judiciously. While they allow you to override TypeScript’s type inference, incorrect assertions can lead to runtime errors, as TypeScript won’t perform actual type checks at runtime based on the assertion.
Debugging TypeScript Applications in VS Code
Debugging is invaluable for troubleshooting and ensuring your code behaves as expected.
-
Enable Source Maps: In your
tsconfig.jsonfile, setsourceMaptotruewithin thecompilerOptionssection. A source map (.js.mapfile) helps map compiled JavaScript code back to your original TypeScript code for debugging. -
Recompile your code: After enabling source maps, recompile your TypeScript code (e.g.,
tsc). You’ll see.js.mapfiles generated alongside your.jsfiles. -
Set Breakpoints: Open your
.tsfile and click on the line number in the gutter to set a breakpoint. Breakpoints pause code execution when reached. -
Create
launch.json:-
Go to the Debug panel in VS Code (usually the “Run and Debug” icon on the left sidebar).
-
Click on “create a launch.json file” (or the gear icon) and select “Node.js” from the dropdown. This configures VS Code for debugging.
-
-
Add
preLaunchTask: In yourlaunch.json, add apreLaunchTasksetting:{ "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Launch Program", "skipFiles": [ "<node_internals>/**" ], "program": "${workspaceFolder}/dist/index.js", // Adjust path if your output is different "preLaunchTask": "tsc: build - tsconfig.json", // This tells VS Code to compile before launching "outFiles": [ "${workspaceFolder}/dist/**/*.js" // Adjust path ] } ] } -
Start Debugging: Go back to your
.tsfile, open the Debug panel, and click “Launch Program” or pressF5. -
Debug Tools: Your program will start and pause at the breakpoint. In the Debug panel, you have tools like:
-
Step Over (F10): Execute one line of code.
-
Step Into: Step into a function call.
-
Step Out: Step out of the current function.
-
Restart: Restart the debugging session.
-
Stop: Terminate debugging.
-
-
Inspect Variables: On the left, under “Variables,” you can see detected variables and their values as you step through the code. You can also add variables to the “Watch” window.
-
Practice: Add
console.logstatements for extra inspection during debugging and useF5to start andF10to step through.
graph TD A[tsconfig.json] -- (set 'sourceMap': true) --> B[TypeScript Compiler] B -- compile .ts to .js + .js.map --> C[.ts file with breakpoints] D[VS Code Debug Panel] -- Create launch.json --> E[launch.json] E -- (set 'preLaunchTask': 'tsc: build') --> B C -- F5 (Launch Program) --> D D -- Debugging Tools --> F[Inspect Variables, Step through code]
Flowchart: Debugging in VS Code
Next Steps to Master TypeScript
-
Practice: The most crucial step to mastering any language is consistent practice. Start writing your own TypeScript code immediately.
-
Implement in Projects: If you’re using JavaScript frameworks (React, Next.js, etc.), begin implementing TypeScript in your new projects to experience its benefits in code quality and early error detection firsthand.
-
Explore Open-Source Projects: Examine how experienced developers use TypeScript in open-source projects on platforms like GitHub or GitLab. This “reverse engineering” helps you understand real-world applications.
-
Consult Official Documentation: Dive into the official TypeScript documentation. It’s an invaluable resource for in-depth knowledge and examples.
-
Deep Dive into Configuration: Explore the
tsconfig.jsonfile further. Learn about its various options and experiment with custom configurations to optimize TypeScript for your specific projects.