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:
Type Annotations and Type Inference:
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 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.
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:
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:
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.
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
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;
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);
You can convert other numeric types to bigint using the BigInt() function.
let num: number = 12345;
let converted: bigint = BigInt(num);
You can create arrays of bigint values.
let bigIntegers: bigint[] = [123n, 456n, 789n];
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:
let isTrue: boolean = true;
let isFalse: boolean = false;
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.");
}
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.");
}
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
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.");
}
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:
let myValue: string = "Hello";
myValue = null; // This is allowed because of null's subtyping behavior
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");
}
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);
}
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
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:
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;
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
In many cases, TypeScript can infer the number type automatically based on the assigned value.
let count = 42; // TypeScript infers count as number
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
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;
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
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;
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:
You can declare string variables using the string type annotation.
let greeting: string = "Hello, TypeScript!";
let username: string = "Alice";
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}`;
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
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"
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"
You can ensure that a variable holds a specific string value using literal types.
let status: "active" | "inactive" = "active";
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:
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 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.
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.
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.
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:
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.
Symbols can be used as property keys in objects:
const obj = {
[mySymbol]: 'Hello, symbol!'
};
console.log(obj[mySymbol]); // Hello, symbol!
Each call to Symbol() creates a new, unique symbol:
const symbol1 = Symbol('description');
const symbol2 = Symbol('description');
console.log(symbol1 === symbol2); // false
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
}
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 }
The type annotation for a symbol is simply symbol:
const mySymbol: symbol = Symbol('description');
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.
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'
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'
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'
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...
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:
// Declaring an array of numbers
let numbers: number[] = [1, 2, 3, 4, 5];
// Declaring an array of strings
let fruits: string[] = ["apple", "banana", "orange"];
let colors = ["red", "green", "blue"]; // TypeScript infers colors as string[]
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]
let person: [string, number] = ["John", 30]; // A tuple with a string and a number
let mixed: (string | number)[] = ["hello", 42, "world", 123];
let values: any[] = [1, "two", true];
let numbers: number[] = values as number[];
let readOnlyNumbers: ReadonlyArray<number> = [1, 2, 3];
let first = numbers[0]; // Accessing array element
let [a, b, ...rest] = numbers; // Destructuring array
let combined = [...numbers, 6, 7, 8]; // Spreading arrays
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:
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.
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.
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.
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;
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" };
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 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:
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:
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 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 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 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
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"
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"
}
}
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:
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:
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
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
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
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
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.
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.
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" };
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);
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.
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:
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.
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.
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.
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:
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
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!
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
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.
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.
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.
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;
}
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
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!
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
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
WebDeveloper work on JavaScript,Typescript, CSS, HTML,frontend developer (Next.js, Tailwind CSS, ShadCN UI)
1yThanks for sharing this knowledge