TypeScript Prodigy: Unlocking the Magic of Key Language Features

TypeScript Prodigy: Unlocking the Magic of Key Language Features


In the dynamic landscape of modern web development, TypeScript stands as a powerful and versatile programming language that enhances JavaScript with a robust system of type annotations and inference. By seamlessly integrating both static and dynamic typing paradigms, TypeScript provides developers with a wide array of features to ensure code reliability, scalability, and maintainability. From foundational concepts like primitive types and objects to more advanced techniques such as interfaces, generics, and type guards, this article serves as an exploration into the core elements that define TypeScript's distinctive approach. Whether you're a seasoned developer looking to dive into TypeScript's intricacies or a newcomer curious about its capabilities, this comprehensive overview will illuminate the language's key features and demonstrate how they synergistically contribute to a more confident and efficient development experience.

Table of contents:

  1. Type Annotations and Type Inference
  2. Primitive Types
  3. Objects
  4. Array
  5. Interfaces
  6. Classes
  7. Enums
  8. Type Aliases
  9. Function Types
  10. Generics
  11. Union Types
  12. Intersection Types
  13. Type guards
  14. Type assertion


Type Annotations and Type Inference:

  • Type Annotations:

Type annotations involve explicitly specifying the type of a variable, function parameter, or function return value using a colon (:) followed by the desired type.

// Type annotation for a variable
let age: number;
age = 25;

// Type annotation for a function parameter and return value
function multiply(a: number, b: number): number {
    return a * b;
}

// Type annotation for an object
let person: { name: string, age: number };
person = { name: "Alice", age: 30 };        

In this code, the type annotations clearly indicate the expected types for variables, function parameters, and object properties.

  • Type Inference:

Type inference is TypeScript’s ability to automatically deduce the type of a variable based on its value and how it’s used in the code. This reduces the need for explicit type annotations.

let username = "john_doe"; // TypeScript infers 'string' type
let count = 42;            // TypeScript infers 'number' type

function add(a: number, b: number) {
    return a + b;          // TypeScript infers return type 'number'
}

let result = add(10, 20); // TypeScript infers 'number' type for 'result'        

TypeScript analyzes the value assignments and function usages to determine the types.

  • Type Annotations vs. Type Inference: 

In some cases, you might need to provide explicit type annotations to make your intentions clear, especially when the inference might not be accurate or when you want to enforce stricter typing.

let temperature: number = 25; // Explicit type annotation

function greet(name: string): string {
    return "Hello, " + name;
}        

In other cases, TypeScript’s type inference can significantly reduce the need for explicit annotations, making the code more concise and readable.

let isLoggedIn = true; // TypeScript infers 'boolean' type

function capitalize(str: string) {
    return str.toUpperCase(); // TypeScript infers return type 'string'
}        



Primitive Types:

  1. BigInt:

Represents arbitrary precision integers. It’s a numeric type that can hold very large integer values that are beyond the range of the standard number type. This is particularly useful when you're dealing with computations that require precision and accuracy for extremely large numbers.

Let’s delve into more detail with code snippets:

  • Defining a bigint Variable:

let bigValue: bigint = 1234567890123456789012345678901234567890n;        

Here, 1234567890123456789012345678901234567890n is a bigint literal. Note the use of the n suffix to indicate that the value should be treated as a bigint.

  •  Arithmetic Operations:

You can perform various arithmetic operations on bigint values just like with regular numbers.

let a: bigint = 100n;
let b: bigint = 200n;

let sum: bigint = a + b;
let difference: bigint = b - a;
let product: bigint = a * b;
let quotient: bigint = b / a; // Returns the integer quotient        

  • Comparisons:

bigint values can be compared using standard comparison operators.

let x: bigint = 123n;
let y: bigint = 456n;

let isGreater: boolean = x > y;
let isEqual: boolean = x === y;        

  • Mixing bigint with other types:

While you can mix bigint with other numeric types, the result will be a bigint.

let a: bigint = 100n;
let b: number = 50;

let result: bigint = a + BigInt(b);        

  • Converting to bigint:

You can convert other numeric types to bigint using the BigInt() function.

let num: number = 12345;
let converted: bigint = BigInt(num);        

  •  Array of bigint:

You can create arrays of bigint values.

let bigIntegers: bigint[] = [123n, 456n, 789n];        

  • Type Annotations:

When a bigint is inferred from context, you don't always need to explicitly annotate the type.

let inferredBigInt = 12345n; // TypeScript infers the type as bigint        


2. Boolean:

Represents values of either true or false. It's often used for conditional statements, logic operations, and controlling the flow of your program. Let's delve into more detail with some code snippets:

  • Declaring boolean variables: You can declare boolean variables using the boolean keyword.

let isTrue: boolean = true;
let isFalse: boolean = false;        

  • Using boolean values in conditionals:

Booleans are frequently used in conditional statements to control the flow of your program.

let isLoggedIn: boolean = true;

if (isLoggedIn) {
  console.log("Welcome, user!");
} else {
  console.log("Please log in.");
}        

  • Logical operators:

Booleans are often used with logical operators such as && (AND), || (OR), and ! (NOT).

let hasPermission: boolean = true;
let isAuthenticated: boolean = false;

if (hasPermission && isAuthenticated) {
  console.log("Access granted.");
} else {
  console.log("Access denied.");
}        

  • Functions returning boolean values:

You can define functions that return boolean values for various purposes.

function isEven(num: number): boolean {
  return num % 2 === 0;
}

console.log(isEven(4));  // true
console.log(isEven(7));  // false        

  • Type checking with boolean:

You can use boolean values for type-checking conditions.

let userInput: string = "hello";
let isValid: boolean = userInput.length > 3;

if (isValid) {
  console.log("Input is valid.");
} else {
  console.log("Input is too short.");
}        

  • Default boolean values:

In JavaScript and TypeScript, values that are considered “truthy” evaluate to true, while "falsy" values evaluate to false. Common falsy values include false, 0, "" (empty string), null, undefined, and NaN.

let emptyString: string = "";
let hasContent: boolean = Boolean(emptyString);  // Evaluates to false        

Remember that TypeScript’s type checking helps catch errors related to boolean values, such as trying to use a non-boolean value in a conditional statement or logical operation.


3. Null:

Represents the absence of a value or the intentional absence of a value. It is a sub-type of all other types, which means you can assign null to variables of any type. However, there are certain rules and considerations when working with null values. Let's explore this in more detail with code snippets:

  • Assigning null to Variables:

let myValue: string = "Hello";
myValue = null;  // This is allowed because of null's subtyping behavior        

  • Checking for null:

You can explicitly check for null using an if statement:

let myValue: string | null = "Hello";
if (myValue === null) {
 console.log("Value is null");
} else {
 console.log("Value is not null");
}        

  • Function Returns null:

You can define a function that returns null explicitly:

function findItem(name: string): string | null {
  // Some logic to find the item, if not found, return null
  return null;
}

let item = findItem("Apple");
if (item === null) {
  console.log("Item not found");
} else {
  console.log("Item found:", item);
}        

  • Strict Null Checks:

TypeScript has a compiler option called strictNullChecks. When enabled, it enforces strict null checking and helps you avoid many null-related errors. For example, without strict null checks enabled, this would be allowed:

let value: string = null;  // This is allowed without strictNullChecks        

With strict null checks enabled, you need to explicitly allow null:

let value: string | null = null;  // This is allowed with strictNullChecks        

  • Non-null Assertion Operator (!):

You can use the non-null assertion operator (!) to tell TypeScript that you are sure a value won't be null or undefined. Use this with caution, as it can lead to runtime errors if the value is actually null:

let username: string | null = getUsernameFromAPI();
let formattedUsername = username!.toUpperCase();  // Using ! to assert non-null        


4. Number

Represents numeric values, whether they are integers or floating-point numbers. Let’s explore it in more detail with code snippets:

  • Defining Number Variables:

You can declare number variables using the number type. TypeScript will enforce that only numeric values can be assigned to these variables.

let age: number = 25;
let temperature: number = 98.6;        

  • Basic Math Operations:

You can perform arithmetic operations on number variables, just like in JavaScript.

let x: number = 10;
let y: number = 5;

let sum: number = x + y;       // 15
let difference: number = x - y; // 5
let product: number = x * y;    // 50
let quotient: number = x / y;   // 2        

  • Type Inference:

In many cases, TypeScript can infer the number type automatically based on the assigned value.

let count = 42;  // TypeScript infers count as number        

  • Number Operations and Methods:

Number variables in TypeScript come with built-in methods and properties.

let num: number = 7.8;

let rounded: number = Math.round(num);     // 8
let squared: number = Math.pow(num, 2);    // 60.84
let absolute: number = Math.abs(-num);     // 7.8
let random: number = Math.random();         // Random number between 0 and 1        

  • NaN and Infinity:

You can represent special numeric values like NaN (Not-a-Number) and Infinity.

let notANumber: number = NaN;
let positiveInfinity: number = Infinity;
let negativeInfinity: number = -Infinity;        

  • Type Compatibility:

In TypeScript, you can mix and match number variables with other numeric types, like bigint.

let bigIntValue: bigint = 100n;
let sumResult: number = x + bigIntValue;  // Still works, but potential precision loss        

  • Type Constraints:

If you want to restrict a number to a certain range, you can use literal types or custom types.

let diceRoll: 1 | 2 | 3 | 4 | 5 | 6 = 4;        

  • Type Annotations:

You can explicitly define the number type using type annotations.

let population: number;
population = 1000000;        


5. String: 

Represents textual data, such as words, sentences, or any sequence of characters. Let’s dive into more detail and explore various ways you can work with strings in TypeScript:

  • Declaring String Variables:

You can declare string variables using the string type annotation.

let greeting: string = "Hello, TypeScript!";
let username: string = "Alice";        

  • Concatenation:

You can concatenate strings using the + operator or by using template literals.

let firstName: string = "John";
let lastName: string = "Doe";

let fullNameConcat: string = firstName + " " + lastName;
let fullNameTemplate: string = `${firstName} ${lastName}`;        

  • String Methods:

TypeScript provides access to string methods like in JavaScript.

let message: string = "Hello, World";

let length: number = message.length; // Get length: 13
let uppercase: string = message.toUpperCase(); // Convert to uppercase
let lowercase: string = message.toLowerCase(); // Convert to lowercase
let substring: string = message.substring(0, 5); // Substring: "Hello"
let indexOfW: number = message.indexOf("W"); // Index of "W": 7
let replaced: string = message.replace("World", "Universe"); // Replace        

  • Accessing Characters:

Strings can be treated like arrays of characters.

let sentence: string = "Hello!";
let firstChar: string = sentence[0]; // "H"
let thirdChar: string = sentence.charAt(2); // "l"        

  • String Literals:

You can use single quotes, double quotes, or backticks for string literals. Backticks allow multiline strings and expression interpolation.

let singleQuote: string = 'Single quotes';
let doubleQuote: string = "Double quotes";
let multiLine: string = `
  Line 1
  Line 2
`;
let interpolated: string = `Sum: ${2 + 3}`; // "Sum: 5"        

  • String Type Checking:

You can ensure that a variable holds a specific string value using literal types.

let status: "active" | "inactive" = "active";        

  • String Template Interpolation:

Template literals (${expression}) enable dynamic value insertion within strings.

let user = {
  firstName: "Alice",
  lastName: "Smith",
};

let userInfo: string = `User: ${user.firstName} ${user.lastName}`;        


6. Undefined:

Represents a value that is not assigned or has not been initialized. It’s often used to indicate the absence of a value. Let’s explore the undefined type in more detail with code snippets:

  • Variable Initialization:

let variableWithoutValue; // implicitly of type 'any'
let variableUndefined: undefined = undefined; // explicitly assigning undefined        

In the first line, variableWithoutValue is declared without an explicit type, so it's considered of type any. In the second line, variableUndefined is explicitly assigned the value undefined and has a type annotation of undefined.

  • Function Return Type:

function findElement(arr: number[], target: number): number | undefined {
  for (const num of arr) {
    if (num === target) {
      return num;
    }
  }
  return undefined;
}

const numbers = [1, 2, 3, 4, 5];
const foundNumber = findElement(numbers, 3);

if (foundNumber !== undefined) {
  console.log("Number found:", foundNumber);
} else {
  console.log("Number not found");
}        

In this example, the findElement function returns a number if the target is found in the array, or undefined if not found. The return type is explicitly specified as number | undefined to indicate that the function can return either a number or undefined.

  • Type Assertion and undefined:

let possiblyUndefined: number | undefined = Math.random() > 0.5 ? 10 : undefined;

// Using type assertion to treat 'possiblyUndefined' as number
let definitelyNumber: number = possiblyUndefined as number;        

Here, possiblyUndefined is a variable that might hold a number or be undefined. We use type assertion (using as) to treat it as a number, but this can potentially lead to runtime errors if possiblyUndefined is indeed undefined.

  • Optional Function Parameters:

function greet(name?: string): string {
  if (name) {
    return `Hello, ${name}!`;
  } else {
    return "Hello, stranger!";
  }
}

console.log(greet());         // Output: Hello, stranger!
console.log(greet("Alice"));  // Output: Hello, Alice!        

In this example, the name parameter in the greet function is optional, indicated by the ? symbol. If no argument is provided for name, it defaults to undefined.

  • Checking for undefined:

let someValue: number | undefined = 42;

if (someValue === undefined) {
  console.log("Value is undefined");
} else {
  console.log("Value is:", someValue);
}        

Here, we’re explicitly checking whether someValue is equal to undefined using the strict equality operator (===).

By using the undefined type and understanding its behavior, you can better handle scenarios where values might be absent or uninitialized in your TypeScript code.


7. Symbol:

Represents a unique and immutable value that can be used as an object property key. It’s often used to create “hidden” or “private” properties on objects, as symbols are not exposed through iteration or enumeration.

Here’s how you can work with the symbol type:

  • Creating Symbols:

You can create a symbol using the Symbol() function:

const mySymbol = Symbol('description');
console.log(mySymbol); // Symbol(description)        

The string 'description' here is an optional description that can help you identify the symbol, but it doesn't affect the symbol's uniqueness.

  • Using Symbols as Property Keys:

Symbols can be used as property keys in objects:

const obj = {
  [mySymbol]: 'Hello, symbol!'
};

console.log(obj[mySymbol]); // Hello, symbol!        

  • Uniqueness of Symbols:

Each call to Symbol() creates a new, unique symbol:

const symbol1 = Symbol('description');
const symbol2 = Symbol('description');

console.log(symbol1 === symbol2); // false        

  • Hidden Properties:

Symbols are not included in normal object iteration, making them useful for creating “hidden” properties:

for (const key in obj) {
  console.log(key); // This won't log the symbol
}        

  • Well-known Symbols:

TypeScript provides a set of well-known symbols that can be used to customize the behavior of built-in operations like iteration and coercion:

const mySymbol = Symbol.iterator;

const arr = [1, 2, 3];
const iterator = arr[mySymbol]();

console.log(iterator.next()); // { value: 1, done: false }        

  • Type Annotations:

The type annotation for a symbol is simply symbol:

const mySymbol: symbol = Symbol('description');        

  • Global Symbol Registry:

Symbols can be shared across different parts of your application using the global symbol registry:

const globalSymbol = Symbol.for('globalSymbol');
const retrievedSymbol = Symbol.for('globalSymbol');

console.log(globalSymbol === retrievedSymbol); // true        

Remember that while symbols offer unique and private property keys, they are not completely hidden from determined efforts to access them. They’re a tool for creating less likely collisions in property keys and providing a level of encapsulation.

Keep in mind that the true utility of symbols is often seen in more complex use cases or libraries where property visibility and encapsulation are crucial. In everyday development, using plain strings as property keys is usually sufficient.


Objects:

Represents any non-primitive value, which means it can be used to describe variables that can hold complex data structures like arrays, functions, and objects. It's a type that's less specific than other types like arrays, tuples, or classes. Let's delve into more detail and see some code snippets to understand the "object" type better.

  • Basic Usage:

You can use the "object" type to define variables that can hold any non-primitive value.

let myObject: object;

myObject = {};             // Valid
myObject = { key: 'value' } // Valid

// The following assignments will result in errors:
// myObject = 42;          // Error: Type '42' is not assignable to type 'object'
// myObject = "hello";     // Error: Type '"hello"' is not assignable to type 'object'        

  • Properties and Methods:

The "object" type doesn't provide access to properties and methods on the object, as it's a very general type.

let myObject: object = { key: 'value' };

// The following line will result in an error:
// console.log(myObject.key); // Error: Property 'key' does not exist on type 'object'        

  • Type Assertion:

You can use type assertion to tell TypeScript that an object conforms to a certain structure. However, this doesn't change the type of the object itself.

let myObject: object = { key: 'value' };

// Type assertion
let specificObject = myObject as { key: string };
console.log(specificObject.key); // Valid

// The following line will still result in an error:
// console.log(myObject.key);      // Error: Property 'key' does not exist on type 'object'        

  • Using "Object" Literal:

If you want to create an object with specific properties, you can use the object literal syntax with the specific property types.

let specificObject: { key: string } = { key: 'value' };

console.log(specificObject.key); // Valid
// The following line will result in an error:
// console.log(specificObject.otherKey); // Error: Property 'otherKey' does not exist on type...        

  • Keyof with "object" Type:

You can use the "keyof" operator with the "object" type to extract keys from objects. However, this is typically more useful with specific object types.

type MyObjectType = { key1: number, key2: string };
type MyObjectKeys = keyof MyObjectType; // "key1" | "key2"        

In general, while the "object" type can be useful in certain scenarios, it's often better to define more specific types when working with objects to take advantage of TypeScript's type-checking capabilities. The "object" type doesn't provide much in the way of type safety when compared to more specific types.


Arrays:

In TypeScript, arrays are a fundamental data structure used to store collections of values of the same type. TypeScript provides various ways to declare and use arrays, and it also offers type annotations and inference to ensure type safety.

Here's an explanation of array types in TypeScript with code snippets:

  • Basic Array Declaration: You can declare an array using the array syntax. You can also use type annotations to specify the type of elements the array will hold.

// Declaring an array of numbers
let numbers: number[] = [1, 2, 3, 4, 5];

// Declaring an array of strings
let fruits: string[] = ["apple", "banana", "orange"];        

  • Array Type Inference: TypeScript can often infer the array type based on the initial values you provide.

let colors = ["red", "green", "blue"]; // TypeScript infers colors as string[]        

  • Array Methods: Arrays have built-in methods that can modify, filter, map, and perform other operations on the array elements.

let numbers = [1, 2, 3, 4, 5];

let doubled = numbers.map(num => num * 2); // doubled: number[] = [2, 4, 6, 8, 10]

let evens = numbers.filter(num => num % 2 === 0); // evens: number[] = [2, 4]        

  • Tuple Types: Arrays in TypeScript can also represent tuples, which are fixed-size arrays where each element can have a different type.

let person: [string, number] = ["John", 30]; // A tuple with a string and a number        

  • Array Union Types: You can use union types to create arrays that can hold values of different types.

let mixed: (string | number)[] = ["hello", 42, "world", 123];        

  • Array Type Assertions: Sometimes, you might need to tell TypeScript about the type of an array explicitly using type assertions.

let values: any[] = [1, "two", true];
let numbers: number[] = values as number[];        

  • Readonly Arrays: You can make arrays read-only to prevent modifications after initialization.

let readOnlyNumbers: ReadonlyArray<number> = [1, 2, 3];        

  • Array Spread and Destructuring: You can use array spread and destructuring to manipulate array elements.

let first = numbers[0]; // Accessing array element
let [a, b, ...rest] = numbers; // Destructuring array
let combined = [...numbers, 6, 7, 8]; // Spreading arrays        

  • Array Methods and Properties: Arrays come with various methods and properties. TypeScript provides type definitions for these methods, which helps with type safety.

numbers.push(6); // Add element to the end
numbers.pop();   // Remove element from the end
numbers.length;  // Property: number of elements in the array        

Remember that TypeScript's type system helps catch type-related errors at compile time, making your code more robust and easier to maintain when working with arrays or any other data types.


Interfaces:

In TypeScript, interfaces are a fundamental concept used to define the structure of objects or classes. They provide a way to describe the shape of an object, specifying which properties and methods it should have. Interfaces are a powerful tool for enforcing type-checking and ensuring that your code adheres to a certain contract. Let's dive into interfaces with some code snippets:

  • Basic Interface:

interface Person {
  firstName: string;
  lastName: string;
  age: number;
}

const person: Person = {
  firstName: "John",
  lastName: "Doe",
  age: 30,
};        

In this example, we've defined an interface Person that describes the structure of an object with properties firstName, lastName, and age. We then create an object person that adheres to this interface.

  • Optional Properties:

interface Car {
  make: string;
  model: string;
  year?: number; // Optional property
}

const myCar: Car = {
  make: "Toyota",
  model: "Camry",
};        

The year property is marked as optional using the ? symbol. Objects of type Car can have or not have the year property.

  • Readonly Properties:

interface Point {
  readonly x: number;
  readonly y: number;
}

const point: Point = { x: 5, y: 10 };
// point.x = 7; // Error: Cannot assign to 'x' because it is a read-only property.        

Readonly properties can't be modified after they're assigned a value.

  • Function Signatures:

Interfaces can also describe the structure of functions:

interface MathFunction {
  (x: number, y: number): number;
}

const add: MathFunction = (a, b) => a + b;
const subtract: MathFunction = (a, b) => a - b;        

  • Extending Interfaces:

Interfaces can extend other interfaces, inheriting their properties:

interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

const myDog: Dog = { name: "Buddy", breed: "Golden Retriever" };        

  • Implementing Interfaces:

Classes can implement interfaces, ensuring they adhere to a specific structure:

interface Shape {
  calculateArea(): number;
}

class Circle implements Shape {
  constructor(private radius: number) {}

  calculateArea() {
    return Math.PI * this.radius ** 2;
  }
}        

Here, the Circle class implements the Shape interface by providing the required calculateArea method.

  • Index Signatures:

Index signatures allow dynamic property names:

interface Dictionary {
  [key: string]: string;
}

const colors: Dictionary = {
  red: "#FF0000",
  green: "#00FF00",
};        

In this example, colors is a dictionary object with string keys and string values.

Interfaces in TypeScript play a crucial role in creating well-defined contracts for your code. They improve maintainability, prevent errors, and make your codebase more readable.


Classes:

Provides a way to define blueprints for creating objects with shared properties and methods. They are used to model real-world entities, and they follow object-oriented programming (OOP) principles like encapsulation, inheritance, and polymorphism.

Defining a Class:

To define a class in TypeScript, you use the class keyword followed by the class name. Here's a basic example of a class definition:

class Animal {
    // Properties
    name: string;
    species: string;

    // Constructor
    constructor(name: string, species: string) {
        this.name = name;
        this.species = species;
    }

    // Method
    makeSound() {
        console.log("Some generic animal sound");
    }
}        

Sure, I'd be happy to explain TypeScript classes in detail along with code snippets!

Classes in TypeScript:

In TypeScript, classes provide a way to define blueprints for creating objects with shared properties and methods. They are used to model real-world entities, and they follow object-oriented programming (OOP) principles like encapsulation, inheritance, and polymorphism.

Defining a Class:

To define a class in TypeScript, you use the class keyword followed by the class name. Here's a basic example of a class definition:

class Animal {
    // Properties
    name: string;
    species: string;

    // Constructor
    constructor(name: string, species: string) {
        this.name = name;
        this.species = species;
    }

    // Method
    makeSound() {
        console.log("Some generic animal sound");
    }
}        

In the above example:

  • name and species are properties of the Animal class.
  • The constructor is a special method that gets called when an object of the class is created. It initializes the properties.
  • makeSound is a method of the class that defines a behavior.

Creating Objects:

You can create instances of a class using the new keyword followed by the class name:

const dog = new Animal("Buddy", "Dog");
console.log(dog.name); // Output: Buddy
console.log(dog.species); // Output: Dog
dog.makeSound(); // Output: Some generic animal sound        

Inheritance:

In TypeScript, classes can inherit properties and methods from other classes. This is achieved using the extends keyword.

class Dog extends Animal {
    constructor(name: string) {
        super(name, "Dog"); // Call the parent class constructor
    }

    // Overriding the method from the parent class
    makeSound() {
        console.log("Woof! Woof!");
    }
}        

Access Modifiers:

TypeScript provides access modifiers that determine the visibility and accessibility of class members (properties and methods). The three access modifiers are:

  • public: The member is accessible from outside the class.
  • protected: The member is accessible within the class and its subclasses.
  • private: The member is only accessible within the class itself.

Code Example with Access Modifiers:

class Person {
    private name: string;
    protected age: number;

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

    greet() {
        console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
    }
}

class Employee extends Person {
    private employeeId: number;

    constructor(name: string, age: number, employeeId: number) {
        super(name, age);
        this.employeeId = employeeId;
    }

    displayInfo() {
        console.log(`Employee ID: ${this.employeeId}`);
    }
}

const employee = new Employee("Alice", 30, 12345);
employee.greet(); // Output: Hello, my name is Alice and I'm 30 years old.
employee.displayInfo(); // Output: Employee ID: 12345        

This example demonstrates the use of access modifiers (private, protected) and inheritance in TypeScript classes.

Remember that TypeScript classes are a powerful tool for structuring your code and modeling real-world concepts in a type-safe manner, making it easier to catch errors during development.


Enums:

An enum is a way to define a set of named constant values that can be assigned to a variable. It provides a convenient way to work with groups of related constants. Enum values are represented as numeric values by default, starting from 0, but you can also assign specific values to enum members if needed.

Here's an explanation of enums in detail with code snippets:

  • Numeric Enums:

Numeric enums assign incremental numeric values to each enum member. Here's an example:

enum Direction {
  North,  // 0
  East,   // 1
  South,  // 2
  West    // 3
}

let dir: Direction = Direction.East;
console.log(dir); // Outputs: 1        

In this example, Direction is an enum with default numeric values. Direction.East has a value of 1. You can also assign values explicitly:

enum Direction {
  North = 10,
  East = 20,
  South = 30,
  West = 40
}

let dir: Direction = Direction.East;
console.log(dir); // Outputs: 20        

  • String Enums:

String enums use string values instead of numeric values for enum members. This can make the code more readable, especially when debugging. Here's an example:

enum LogLevel {
  Info = "INFO",
  Warning = "WARNING",
  Error = "ERROR"
}

let logLevel: LogLevel = LogLevel.Warning;
console.log(logLevel); // Outputs: WARNING        

  • Heterogeneous Enums:

Heterogeneous enums allow the mixing of both numeric and string values within the same enum. However, this practice is less common and may lead to confusion, so use it sparingly.

enum Result {
  Success = 0,
  Failure = "FAILURE"
}

console.log(Result.Success); // Outputs: 0
console.log(Result.Failure); // Outputs: FAILURE        

  • Reverse Mapping:

Enums in TypeScript support reverse mapping, which allows you to obtain the enum member based on its value. This works for numeric enums and string enums.

enum Direction {
  North,
  East,
  South,
  West
}

let value = 2;
let direction = Direction[value];
console.log(direction); // Outputs: "South"        

  • Iterating Over Enum Members:

You can iterate over enum members using for...in loop:

enum Color {
  Red,
  Green,
  Blue
}

for (let key in Color) {
  if (isNaN(Number(key))) {
    console.log(key); // Outputs: "Red", "Green", "Blue"
  }
}        

  • Use Cases:

Enums are useful for representing a fixed set of options or states, such as days of the week, directions, status codes, etc. They provide improved readability, type safety, and better autocomplete suggestions in IDEs.

Remember that TypeScript enums are not a substitute for string literal types or union types, especially when you need a more flexible set of values.

In summary, enums in TypeScript provide a way to define named constant values, making your code more structured and readable. They offer various options for numeric and string values, and reverse mapping, and are commonly used for representing a set of related options or states.


Type Aliases:

Type aliases allow you to create custom names for types, making your code more readable, maintainable, and expressive. They don't create new types; they just provide a way to reference existing types with different names. This is especially useful for complex types or to make your code more self-documenting.

Here's how you can define and use type aliases:

// Basic Type Alias
type Age = number;
type Name = string;

const age: Age = 25;
const name: Name = "John";

// Complex Type Alias
type Point = {
  x: number;
  y: number;
};

const point: Point = { x: 10, y: 20 };

// Function Type Alias
type MathFunction = (x: number, y: number) => number;

const add: MathFunction = (a, b) => a + b;
const multiply: MathFunction = (a, b) => a * b;

// Union Type Alias
type Result = number | string;

const result1: Result = 42;
const result2: Result = "success";

// Intersection Type Alias
type Coordinates = {
  latitude: number;
  longitude: number;
};

type PointWithCoordinates = Point & Coordinates;

const location: PointWithCoordinates = {
  x: 15,
  y: 25,
  latitude: 40.7128,
  longitude: -74.0060,
};

// Generic Type Alias
type Pair<T> = {
  first: T;
  second: T;
};

const numberPair: Pair<number> = { first: 1, second: 2 };
const stringPair: Pair<string> = { first: "hello", second: "world" };        

In the above examples:

  1. We define a basic type alias for Age and Name to represent number and string types respectively.
  2. The Point type alias defines a complex type with x and y properties.
  3. The MathFunction type alias represents a function that takes two numbers and returns a number.
  4. The Result type alias is a union of number and string.
  5. The Coordinates type alias represents latitude and longitude values.
  6. The PointWithCoordinates type alias is an intersection of Point and Coordinates.
  7. The Pair type alias is a generic type that can hold a pair of values of any type.

Using type aliases can make your code more descriptive and easier to understand, especially when dealing with complex types. They also facilitate code reusability and maintainability by providing a central place to define types that might be used in multiple parts of your application.


Function Types:

function types allow you to define the shape and signature of a function. This is particularly useful when you want to specify the type of functions that can be assigned to variables, passed as arguments, or returned from other functions. There are several ways to define function types in TypeScript.

Let's go through the different ways to define and use function types with code snippets:

  • Type Annotations for Function Parameters and Return Types:

You can define function types using type annotations directly in parameter lists and return types:

// Function type annotation
type AddFunction = (a: number, b: number) => number;

// Function using the defined type
const add: AddFunction = (a, b) => a + b;

console.log(add(2, 3)); // Outputs: 5        

  • Function Type Interfaces:

You can use interfaces to define function types as well:

// Function type interface
interface MathFunction {
  (a: number, b: number): number;
}

// Function using the defined interface
const subtract: MathFunction = (a, b) => a - b;

console.log(subtract(5, 3)); // Outputs: 2        

  • Function Type as Parameter:

You can use function types as parameters to other functions:

// Function that takes a function as a parameter
function calculate(operation: MathFunction, a: number, b: number): number {
  return operation(a, b);
}

console.log(calculate(add, 4, 6));      // Outputs: 10
console.log(calculate(subtract, 8, 3)); // Outputs: 5        

  • Function Type as Return Type:

You can also use function types as the return type of another function:

// Function that returns a function
function getOperation(operationName: string): MathFunction {
  if (operationName === "add") {
    return add;
  } else {
    return subtract;
  }
}

const operation = getOperation("add");
console.log(operation(7, 3)); // Outputs: 10        

  • Optional and Rest Parameters:

You can define optional and rest parameters in function types:

type ConcatenateFunction = (str1: string, str2?: string, ...rest: string[]) => string;

const concatenate: ConcatenateFunction = (str1, str2, ...rest) => {
  let result = str1 + (str2 || "");
  result += rest.join("");
  return result;
}

console.log(concatenate("Hello", " TypeScript", " Fans")); // Outputs: "Hello TypeScript Fans"        

These are some of the ways you can define and use function types in TypeScript. They allow you to enforce type safety when working with functions and provide clear expectations for function signatures.


Generics:

Generics in TypeScript allow you to create reusable components or functions that can work with different types of data without sacrificing type safety. They enable you to write more flexible and reusable code by parameterizing types.

  • Basic Generic Functions:

Let's start with a basic example of a generic function that swaps the values of two variables. This function should work with various types, such as numbers, strings, etc.

function swap<T>(a: T, b: T): void {
    let temp: T = a;
    a = b;
    b = temp;
}

let a: number = 5;
let b: number = 10;
swap(a, b);
console.log(a, b);  // Output: 5 10

let x: string = "hello";
let y: string = "world";
swap(x, y);
console.log(x, y);  // Output: hello world        

In this example, the <T> syntax inside the function indicates that it's using a generic type T. The function swap can now work with different types while preserving type safety.

  • Generic Interfaces:

You can also create generic interfaces to describe object structures that work with different types of data.

interface Box<T> {
    value: T;
}

let boxOfNumbers: Box<number> = { value: 42 };
let boxOfStrings: Box<string> = { value: "hello" };        

  • Generic Classes:

Generics can also be applied to classes, allowing you to create reusable classes that work with various types.

class Pair<T, U> {
    constructor(public first: T, public second: U) {}
}

let numberAndString: Pair<number, string> = new Pair(42, "answer");
let booleanAndNumber: Pair<boolean, number> = new Pair(true, 3);        

  • Constraints on Generics:

You can apply constraints on generics to limit the types that can be used. For example, you might want to ensure that the type being used supports a particular operation.

interface Lengthy {
    length: number;
}

function printLength<T extends Lengthy>(item: T): void {
    console.log(item.length);
}

printLength("hello");       // Output: 5
printLength([1, 2, 3]);     // Output: 3
printLength({ length: 10 }); // Output: 10        

In this example, the T extends Lengthy constraint ensures that the type T passed to the printLength function has a length property.

  • Using Type Parameters in Callbacks:

Generics are commonly used in scenarios involving callbacks, such as with arrays' map function.

function mapArray<T, U>(array: T[], callback: (item: T) => U): U[] {
    return array.map(callback);
}

let numbers = [1, 2, 3, 4, 5];
let squaredNumbers = mapArray(numbers, (num) => num * num);
console.log(squaredNumbers);  // Output: [1, 4, 9, 16, 25]        

In this example, the mapArray function can work with any type of array and callback, providing a generic solution.

Generics in TypeScript provide powerful tools for creating flexible, reusable, and type-safe code. They enable you to create functions, interfaces, and classes that can work with different types while maintaining strong typing.


Union Type:

It is a way to define a variable, parameter, or return type that can hold values of multiple different types. It allows you to express that a value could be one of several types. This is particularly useful when you have a function or variable that can accept or return different types of data.

Here's a detailed explanation with code snippets to illustrate union types in TypeScript:

// Defining a Union Type
type MyUnion = string | number;

// Using a Union Type
function displayValue(value: MyUnion) {
    console.log(value);
}

displayValue("Hello");  // Valid
displayValue(42);       // Valid
// displayValue(true);   // Error - boolean is not part of MyUnion

// Union Type with Arrays
type NumberOrStringArray = Array<number> | Array<string>;

function printArray(arr: NumberOrStringArray) {
    console.log(arr);
}

printArray([1, 2, 3]);       // Valid
printArray(["a", "b", "c"]); // Valid
// printArray([true, false]); // Error - boolean[] is not part of NumberOrStringArray

// Union Type with Return Values
function getStringOrNumber(condition: boolean): MyUnion {
    return condition ? "Hello" : 42;
}

const result1: MyUnion = getStringOrNumber(true);  // Valid
const result2: MyUnion = getStringOrNumber(false); // Valid

// Union Type with Object Properties
type Shape = { kind: "circle"; radius: number } | { kind: "square"; sideLength: number };

function area(shape: Shape): number {
    if (shape.kind === "circle") {
        return Math.PI * shape.radius ** 2;
    } else {
        return shape.sideLength ** 2;
    }
}

const circle = { kind: "circle", radius: 5 };
const square = { kind: "square", sideLength: 4 };

console.log(area(circle)); // Calculates the area of a circle
console.log(area(square)); // Calculates the area of a square        

In this example, MyUnion is a union type that can hold values of either string or number. The displayValue function can accept arguments of type MyUnion.

The NumberOrStringArray union type can hold an array of either number or string elements. The printArray function can accept arrays of this type.

The getStringOrNumber function returns a value that can be either a string or a number, based on a condition.

The Shape union type represents either a circle or a square, each with their respective properties. The area function calculates the area of the provided shape.

Union types are powerful tools for creating flexible and expressive type systems in TypeScript, enabling you to work with different data types in a type-safe manner.


Intersection:

An intersection type allows you to combine multiple types into one, creating a new type that has all the properties and methods of the constituent types. This can be very useful when you want to work with objects that have characteristics of multiple types. The syntax for defining an intersection type is to use the & symbol between the types you want to intersect.

Let’s go through some code snippets to understand intersection types in detail:

  • Basic Intersection Type:

type Person = {
  name: string;
};

type Employee = {
  employeeId: number;
};

type PersonAndEmployee = Person & Employee;

const personAndEmployee: PersonAndEmployee = {
  name: "John Doe",
  employeeId: 12345,
};        

In this example, the Person and Employee types are intersected to create a new type PersonAndEmployee, which has properties from both types. The personAndEmployee object can have both name and employeeId properties.

  • Function Intersection:

type Logger = {
  log: (message: string) => void;
};

type Validator = {
  validate: (input: unknown) => boolean;
};

type LoggerAndValidator = Logger & Validator;

const loggerAndValidator: LoggerAndValidator = {
  log: (message) => {
    console.log(message);
  },
  validate: (input) => {
    return typeof input === "string";
  },
};        

Here, the Logger and Validator types are intersected to create a new type LoggerAndValidator. The loggerAndValidator object can both log messages and validate inputs.

  • Intersection with Existing Types:

type Point = {
  x: number;
  y: number;
};

type Color = {
  color: string;
};

type ColoredPoint = Point & Color;

function drawPoint(point: ColoredPoint) {
  console.log(`Drawing a ${point.color} point at (${point.x}, ${point.y})`);
}

const coloredPoint: ColoredPoint = {
  x: 10,
  y: 20,
  color: "red",
};

drawPoint(coloredPoint);        

In this example, the Point and Color types are intersected to create a ColoredPoint type. The drawPoint function takes a ColoredPoint object and draws it using the combined properties.

  • Intersection with Union Types:

type Admin = {
  isAdmin: true;
};

type User = {
  isAdmin: false;
  username: string;
};

type AdminOrUser = Admin & User;

const adminUser: AdminOrUser = {
  isAdmin: true,
  username: "admin123",
};        

Here, the Admin and User types are intersected to create a type AdminOrUser. Notice that the isAdmin property's type is a union of true and false, reflecting the combined possibilities from both types.

Intersection types are powerful tools for combining various characteristics from different types. They’re particularly handy in scenarios where you need to work with objects that possess multiple sets of features.


Type guards:

Type guards allow you to narrow down the type of a variable within a certain code block or conditional statement. They are used to make the TypeScript compiler more aware of the types and enable better type inference.

TypeScript uses type guards to perform narrowing of types based on runtime checks, helping you write safer and more predictable code. There are several ways to implement type guards:

  • typeof Type Guards:

The typeof operator can be used to narrow down the type of a variable based on its primitive value.

function printLength(value: string | number) {
    if (typeof value === "string") {
        console.log("String length:", value.length); // TypeScript knows value is a string here
    } else {
        console.log("Number:", value); // TypeScript knows value is a number here
    }
}

printLength("hello"); // String length: 5
printLength(42);      // Number: 42        

  • instanceof Type Guards:

The instanceof operator can be used to narrow down the type of an object based on its constructor.

class Dog {
    bark() {
        console.log("Woof woof!");
    }
}

class Cat {
    meow() {
        console.log("Meow!");
    }
}

function petSound(animal: Dog | Cat) {
    if (animal instanceof Dog) {
        animal.bark(); // TypeScript knows animal is a Dog here
    } else {
        animal.meow(); // TypeScript knows animal is a Cat here
    }
}

petSound(new Dog()); // Woof woof!
petSound(new Cat()); // Meow!        

  • User-Defined Type Guards:

You can define your own functions that act as type guards using the is keyword in the return type.

interface Square {
    kind: "square";
    size: number;
}

interface Circle {
    kind: "circle";
    radius: number;
}

function isCircle(shape: Square | Circle): shape is Circle {
    return shape.kind === "circle";
}

function getArea(shape: Square | Circle) {
    if (isCircle(shape)) {
        return Math.PI * shape.radius ** 2;
    } else {
        return shape.size ** 2;
    }
}

const circle: Circle = { kind: "circle", radius: 5 };
const square: Square = { kind: "square", size: 4 };

console.log(getArea(circle)); // 78.53981633974483
console.log(getArea(square)); // 16        

  • Type Predicates:

TypeScript allows you to define type predicates using custom functions that return a type assertion.

function isString(value: any): value is string {
    return typeof value === "string";
}

function processValue(value: any) {
    if (isString(value)) {
        console.log(value.toUpperCase()); // TypeScript knows value   is a string here
    } else {
        console.log(value); // TypeScript knows value is not a string here
    }
}

processValue("hello"); // HELLO
processValue(42);      // 42        

TypeScript’s type guards provide a powerful way to work with different types in a type-safe manner, enhancing code correctness and reducing potential runtime errors.


Type Assertion:

a type assertion is a way to tell the compiler that you, as the programmer, know more about the type of a value than the compiler does. It’s a way to override the default type inference or to indicate that a value conforms to a specific type even though the compiler might not be able to determine it on its own. Type assertions are sometimes referred to as “type casting” or “type coercion” in other programming languages.

Type assertions are not type conversions at runtime; they are purely a compile-time construct for the TypeScript compiler to understand your intentions about the types. If the type assertion is incorrect and the value’s actual type doesn’t match the asserted type, you may encounter runtime errors.

Type assertions can be done in two ways: using the as keyword or using the angle bracket (< >) syntax. Let's explore both with code snippets.

  • Using the as keyword:

let someValue: any = "Hello, TypeScript!";
let strLength: number = (someValue as string).length;
console.log(strLength); // Output: 19        

In this example, someValue is of type any, which means TypeScript doesn't have any type information about it. We assert that someValue is a string using the as keyword, allowing us to access the length property.

  • Using the angle bracket (< >) syntax:

let someValue: any = "Hello, TypeScript!";
let strLength: number = (<string>someValue).length;
console.log(strLength); // Output: 19        

The angle bracket syntax is the older way of doing type assertions and might conflict with JSX syntax in some cases, so using the as keyword is generally recommended.

Here’s another example using interfaces:

interface Animal {
    name: string;
}

let animal: Animal = { name: "Lion" };

// Incorrect type assertion
let incorrectAssertion: any = animal as string;
console.log(incorrectAssertion.toUpperCase()); // Runtime error

// Correct type assertion
let correctAssertion: any = animal as Animal;
console.log(correctAssertion.name); // Output: Lion        

In the above example, we initially try to assert animal as a string, which leads to a runtime error because animal is an object with the name property, not a string. The correct type assertion is to assert it as an Animal object.

Remember that using type assertions should be done carefully and only when you are confident about the type compatibility. In cases where you’re not entirely sure about the type, consider using type guards or other type-safe approaches to handle your data.


Modules in TypeScript:

Modules in TypeScript provide a way to organize code into separate files and namespaces, making it easier to manage and scale your projects. They allow you to encapsulate code, prevent naming conflicts, and provide better code organization.

  • Exporting from a Module:

In TypeScript, you can export various entities from a module using the export keyword. Here's how you can export variables, functions, classes, and interfaces:

// mathUtils.ts
export const add = (a: number, b: number): number => a + b;

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

export class Calculator {
    multiply(a: number, b: number): number {
        return a * b;
    }
}

export interface Shape {
    area(): number;
}        

  • Importing from a Module:

You can import the exported entities into another file using the import keyword:

// app.ts
import { add, subtract, Calculator } from './mathUtils';

console.log(add(5, 3));  // Output: 8
console.log(subtract(10, 4));  // Output: 6

const calc = new Calculator();
console.log(calc.multiply(2, 3));  // Output: 6        

You can also use the as keyword to alias imports:

import { add as addition } from './mathUtils';
console.log(addition(2, 2));  // Output: 4        

  • Default Exports:

You can have one default export per module. This can be a class, function, object, etc. When importing a default export, you can name it whatever you want:

// logger.ts
export default class Logger {
    log(message: string): void {
        console.log(message);
    }
}

// app.ts
import MyLogger from './logger';
const logger = new MyLogger();
logger.log("Hello, TypeScript!");  // Output: Hello, TypeScript!        

  • Re-exports:

You can also re-export entities from one module to another, allowing you to create higher-level abstractions:

// utilities.ts
export function capitalize(str: string): string {
    return str.charAt(0).toUpperCase() + str.slice(1);
}

// helpers.ts
export * from './utilities';  // Re-export all from utilities.ts        

  • Using Namespaces:

Namespaces are a way to group related code. They provide a level of organization and can help prevent naming conflicts:

// shapes.ts
namespace Shapes {
    export class Circle {
        constructor(public radius: number) {}
        area(): number {
            return Math.PI * this.radius * this.radius;
        }
    }
}

// app.ts
const circle = new Shapes.Circle(5);
console.log(circle.area());  // Output: Approximately 78.54        

Module Resolution:

TypeScript uses module resolution strategies to locate and load modules. Common strategies are Classic (for Node.js-like environments) and Node (for CommonJS). You can configure the module resolution in your tsconfig.json file.

These examples showcase the fundamental concepts of TypeScript modules. They help you organize, encapsulate, and reuse code effectively, making your projects more maintainable and scalable.


#TypeAnnotationsandTypeInference #PrimitiveTypes #Objects #Array #Interfaces #Classes #Enums #TypeAliases #FunctionTypes #Generics #UnionTypes #IntersectionTypes #Typeguards #Typeassertion #module


Sara A.

WebDeveloper work on JavaScript,Typescript, CSS, HTML,frontend developer (Next.js, Tailwind CSS, ShadCN UI)

1y

Thanks for sharing this knowledge

Like
Reply

To view or add a comment, sign in

More articles by Abdulmoiz Ahmer

Explore topics