A Complete Guide - TypeScript Working with Multiple Types
TypeScript Working with Multiple Types
TypeScript offers a powerful type system that allows developers to work with multiple types in a variety of ways, promoting flexibility and precision in code. Understanding how TypeScript handles multiple types is essential for writing robust, type-safe code. This section will delve into the details of how TypeScript allows different types to coexist and interact seamlessly.
Union Types
Description:
Union types allow you to express a value that can be one of several types. You use the pipe (|
) character to separate the different types. For example, number | string
means that the value can be either a number
or a string
.
Syntax:
let value: number | string;
value = 42; // Valid
value = "Hello"; // Valid
value = true; // Error, boolean is not a valid type
Use Cases:
Union types are particularly useful when dealing with functions that can accept multiple types:
function printId(id: number | string) { console.log(`ID: ${id}`);
}
printId(101); // Valid
printId("user1"); // Valid
Intersection Types
Description:
Intersection types allow you to combine multiple types into a single type. This is achieved using the ampersand (&
) character. The resulting type will have all the properties and methods of the types being intersected.
Syntax:
interface Person { name: string;
} interface Employee { jobTitle: string;
} type EmployeeDetails = Person & Employee; let employee: EmployeeDetails = { name: "Alice", jobTitle: "Developer"
};
Use Cases:
Intersection types are useful for creating flexible and reusable types that combine the properties of several related types:
function extendPerson(person: Person, employee: Employee): EmployeeDetails { return { ...person, ...employee };
}
Type Guards
Description:
Type guards are used to ensure that the type of a variable within a specific block of code is known. TypeScript provides several ways to create type guards, such as typeof
checks, instanceof
checks, and custom type predicates.
Syntax:
function printLength(value: string | number) { if (typeof value === "string") { console.log(`Length: ${value.length}`); } else { console.log(`Number: ${value}`); }
}
Use Cases:
Type guards are crucial for working with union types, allowing you to safely perform operations based on the specific type of the variable:
function isString(value: any): value is string { return typeof value === "string";
} function processInput(input: string | number) { if (isString(input)) { console.log(`String: ${input.toUpperCase()}`); } else { console.log(`Number: ${input * 2}`); }
}
Generics with Multiple Types
Description:
Generics in TypeScript are used to create components and functions that can work over a variety of types while also being type-safe. You can use generics with multiple types to create more flexible and reusable code.
Syntax:
function merge<T, U>(obj1: T, obj2: U): T & U { return Object.assign({}, obj1, obj2);
} const person = { name: "Alice" };
const employee = { jobTitle: "Developer" }; const result = merge(person, employee);
console.log(result.name); // "Alice"
console.log(result.jobTitle); // "Developer"
Use Cases:
Generics with multiple types are useful for creating utility functions and data structures that can handle various input types while maintaining type safety:
interface Tagged<T> { tag: string; data: T;
} function createTagged<T>(tag: string, data: T): Tagged<T> { return { tag, data };
} const taggedNumber = createTagged<number>("number", 42);
const taggedString = createTagged<string>("string", "Hello");
Key Takeaways
- Union Types (
|
): Allow variables to hold values of multiple types, enhancing flexibility. - Intersection Types (
&
): Combine multiple types into one, useful for creating composite types. - Type Guards: Ensure type safety in union types by checking and narrowing down the type within control flow.
- Generics: Create reusable components and functions that can operate across various types while maintaining type safety.
Conclusion:
Online Code run
Step-by-Step Guide: How to Implement TypeScript Working with Multiple Types
Union Types
Union types allow a variable or a function parameter to hold multiple different types.
Example 1: Basic Union Type
Step 1: Define a function that accepts either a string or a number.
// Define a function that can accept either a string or a number
function printId(id: string | number) { console.log("Your ID is: " + id);
} // Call the function with different types
printId("ABC"); // Output: Your ID is: ABC
printId(123); // Output: Your ID is: 123
Step 2: Narrow the types inside the function.
Sometimes, it's necessary to know which type is being handled. You can do this using type guards.
function printIdWithNarrowing(id: string | number) { if (typeof id === "string") { console.log("Your ID is a string: " + id.toUpperCase()); } else { console.log("Your ID is a number: " + id.toFixed(2)); }
} // Call the function with different types
printIdWithNarrowing("abc"); // Output: Your ID is a string: ABC
printIdWithNarrowing(456); // Output: Your ID is a number: 456.00
Example 2: Union Type in Arrays
Step 1: Create a mixed-type array.
// Define an array that can hold either strings or numbers
const data: (string | number)[] = ["apple", 2, "banana", 4]; // Loop through the array and print each elements
data.forEach(item => { console.log(item);
});
// Output:
// apple
// 2
// banana
// 4
Intersection Types
Intersection types combine multiple types into one, creating a new type that has all the properties of each of the types involved.
Example 1: Basic Intersection Type
Step 1: Define two separate interfaces.
interface Employee { employeeId: number; name: string;
} interface Manager { managerId: number; department: string;
}
Step 2: Combine those interfaces into one intersection type.
// Define an intersection type that merges both Employee and Manager properties
type ManagerEmployee = Employee & Manager; const ceo: ManagerEmployee = { employeeId: 1, name: "Alice Smith", managerId: 1, department: "General Management"
}; console.log(ceo.name + " works in the " + ceo.department); // Output: Alice Smith works in the General Management
Example 2: Intersection Types in Classes
Step 1: Define two separate classes.
Using intersection types doesn’t directly work on classes, however, you can use the concept in inheritance.
class Vehicle { drive() { console.log("Driving..."); }
} class Boat { sail() { console.log("Sailing..."); }
} class Amphicar extends Vehicle implements Boat { sail() { console.log("Amphicar sailing..."); } swim() { console.log("Amphicar swimming..."); }
} const myVehicle: Amphicar = new Amphicar();
myVehicle.drive(); // Output: Driving...
myVehicle.sail(); // Output: Amphicar sailing...
myVehicle.swim(); // Output: Amphicar swimming...
Step 2: Mix behaviors at runtime:
Instead, intersection types can be used when dealing with objects and functions at runtime.
const vehicleInstance: Vehicle = new Vehicle();
const boatInstance: Boat = { sail: () => { console.log("Boat sailing..."); }
} const combinedVehicle = Object.assign({}, vehicleInstance, boatInstance); function operate(vehicle: Vehicle & Boat) { vehicle.drive(); vehicle.sail();
} operate(combinedVehicle);
// Output:
// Driving...
// Boat sailing...
Type Guards
Type guards are a pattern of working with union types that leverages type predicates to narrow the type of variables during runtime.
Example 1: Type Guard Function
Step 1: Define an interface and a function that checks if an object adheres to that interface.
interface Fish { swim: () => void;
} interface Bird { fly: () => void;
} // Type guard function
function isFish(pet: Fish | Bird): pet is Fish { return (pet as Fish).swim !== undefined;
}
Step 2: Use this type guard to safely work with the union types.
const fish: Fish = { swim: () => { console.log("Fish swimming..."); }
} const bird: Bird = { fly: () => { console.log("Bird flying..."); }
} function interactWithPet(pet: Fish | Bird) { if (isFish(pet)) { // Inside here, TypeScript knows that 'pet' is of type 'Fish' pet.swim(); } else { // Inside here, TypeScript knows that 'pet' is of type 'Bird' pet.fly(); }
} interactWithPet(fish); // Output: Fish swimming...
interactWithPet(bird); // Output: Bird flying...
Example 2: in
Keyword as Type Guard
You can also use the in
keyword as a type guard to check if a particular property exists in an object.
function move(pet: Fish | Bird) { if ("swim" in pet) { // Inside here, TypeScript knows that 'pet' must be of type 'Fish' pet.swim(); } else { // Inside here, TypeScript knows that 'pet' must be of type 'Bird' pet.fly(); }
} move(fish); // Output: Fish swimming...
move(bird); // Output: Bird flying...
Example 3: Discriminated Unions
A powerful pattern in TypeScript is to have a common field among union types that helps identify which variant of the type is currently being used.
Step 1: Define the discriminated union.
interface Circle { kind: "circle"; radius: number;
} interface Square { kind: "square"; sideLength: number;
} type Shape = Circle | Square;
Step 2: Use the switch
statement to narrow down the type.
Top 10 Interview Questions & Answers on TypeScript Working with Multiple Types
Top 10 Questions and Answers: TypeScript Working with Multiple Types
1. What are union types in TypeScript, and how do you use them?
let id: number | string;
id = 101;
id = "TS101";
In this example, the variable id
can be either a number
or a string
.
2. Can you explain intersection types in TypeScript?
Answer:
Intersection types allow you to combine multiple types into one. An intersection type contains all members of the types it intersects. You use the &
operator to define intersection types:
type Employee = { name: string; id: number;
}; type Engineer = { skills: string[];
}; type FullTimeEmployee = Employee & Engineer; const emp: FullTimeEmployee = { name: "John", id: 202, skills: ["TypeScript", "React"]
};
Here, FullTimeEmployee
requires a combination of properties from both Employee
and Engineer
.
3. How do you create a type guard in TypeScript?
Answer:
Type guards help TypeScript narrow down the type within a conditional block. You can implement a user-defined type guard by defining a function that returns a type predicate (parameterName is Type
):
function isString(input: any): input is string { return typeof input === "string";
} let sample: string | number = "Hello"; if (isString(sample)) { // TypeScript now knows that 'sample' is a string within this block console.log(sample.toUpperCase());
}
4. Can you provide an example of using the typeof
type guard in TypeScript?
Answer:
Yes, the typeof
operator can be used as a type guard to narrow down the type of a variable:
function printValue(value: string | number) { if (typeof value === "string") { console.log(`String value: ${value}`); } else if (typeof value === "number") { console.log(`Number value: ${value}`); }
} printValue("Hello"); // Output: String value: Hello
printValue(42); // Output: Number value: 42
5. What are literal types in TypeScript, and how do they work with union types?
Answer: Literal types restrict a variable to a specific value. When combined with union types, literals allow for defining a finite set of acceptable values:
type Direction = "Up" | "Down" | "Left" | "Right"; let move: Direction; move = "Up"; // Valid
move = "Right"; // Valid
// move = "Diagonal"; // Error: Type '"Diagonal"' is not assignable to type 'Direction'
6. How can you use the in
operator as a type guard in TypeScript?
Answer:
The in
operator checks if a property exists within an object, serving as a type guard:
type Fish = { swim: () => void;
}; type Bird = { fly: () => void;
}; function move(animal: Fish | Bird) { if ("swim" in animal) { return animal.swim(); } return animal.fly();
}
7. What are the benefits of using type aliases in TypeScript?
Answer: Type aliases provide a way to name a type for easier reference and readability. Benefits include:
- Reusability: Create a reusable definition.
- Simplification: Simplify complex types.
- Readability: Make type definitions more understandable.
Example:
type UserID = number | string; function printUserId(id: UserID) { console.log(id);
} printUserId(101); // Number type
printUserId("TS101"); // String type
8. Can you explain the differences between interface
and type
in TypeScript and when to use each?
Answer:
Both interface
and type
in TypeScript can define object shapes, but they have key differences:
- Extending: Interfaces can be extended using the
extends
keyword, while types are extended using intersections (&
). - Declaration Merging: Interfaces can be declared multiple times and merged, while types cannot.
Interface Example:
interface User { name: string;
} interface Admin extends User { role: string;
}
Type Example:
type User = { name: string;
}; type Admin = User & { role: string;
};
Use interfaces
when you need declaration merging or intend to use extends
. Use types
for utility types or when creating simple object shapes.
9. How do you work with optional types in TypeScript?
Answer:
Optional types are used to denote that a property or parameter may or may not be present. In TypeScript, you achieve this by appending a question mark (?
) to the property name in an interface or object type:
interface Person { firstName: string; lastName?: string;
} const person1: Person = { firstName: "John" }; // Valid
const person2: Person = { firstName: "John", lastName: "Doe" }; // Also valid
10. Can you explain how to use tuple types in TypeScript, and why they are useful?
Answer: Tuple types allow you to define an array with a fixed number of elements, where each element may have a different type. Tuples are useful for arrays of fixed sizes with known types for each index:
type UserLogin = [string, number]; // [username, userId] const johnDoe: UserLogin = ["john.doe", 101]; function loginUser(loginDetails: UserLogin) { console.log(`Logged in user: ${loginDetails[0]}, ID: ${loginDetails[1]}`);
} loginUser(johnDoe);
Tuple example:
type Response = [number, string, boolean]; const fetchData = (): Response => [200, "Success", true];
Tuples are particularly handy in APIs that return fixed-size arrays of varying types, enhancing readability and type safety.
Login to post a comment.