Typescript Union And Intersection Types Complete Guide
Understanding the Core Concepts of TypeScript Union and Intersection Types
TypeScript Union Types
Union types in TypeScript are utilized to create a type that can be one of many types combined using the pipe (|
) symbol. This is particularly handy when a value might come in multiple forms and we want to enforce that a variable, argument or return type can be of more than one type.
Importance
Union types allow the code to be more expressive and error-resistant. They help catch errors at compile time and allow for handling multiple data types in a function or variable declaration without resorting to any
or unknown
.
Examples
let errorCode: number | string;
errorCode = 500; // Valid number assignment
errorCode = "404"; // Valid string assignment
In this example, the errorCode
variable can either be a number
or a string
. If you assign a different type of value, TypeScript will throw an error.
Another common use of Union Types is in functions where multiple parameter types are valid:
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
Here, the padLeft
function allows the second parameter to be either a string or a number. It checks the type of padding
to determine the appropriate processing.
TypeScript Intersection Types
Intersection types in TypeScript create a type by combining multiple types using the &
symbol. Intersection types are often used to combine existing object types to create a new type with all the properties of the existing types. This technique is useful when two or more existing types should have a combination of features.
Importance
Intersection types are essential for creating more complex and sophisticated types. They enable building a composite type that combines the properties of multiple types into a single one, which is a powerful tool for type safety and ensuring that objects have all necessary attributes and functionalities.
Examples
interface Drivable {
drive(): void;
}
interface Flyable {
fly(): void;
}
type AirVehicle = Drivable & Flyable;
let vehicle: AirVehicle = {
drive: () => console.log("Vroom vroom"),
fly: () => console.log("Zoom zoom")
};
In the example above, AirVehicle
is a type that combines the Drivable
and Flyable
types. So an object of type AirVehicle
must implement both drive
and fly
methods.
Intersection types are also commonly used to extend the properties of objects:
type Person = {
name: string;
age: number;
};
type Employee = {
employeeId: string;
position: string;
};
type EmployeePerson = Person & Employee;
let employeePerson: EmployeePerson = {
name: "John",
age: 30,
employeeId: "E123456",
position: "Software Developer"
};
Here, EmployeePerson
has properties from both Person
and Employee
making it a type that combines attributes from both.
Conclusion
Online Code run
Step-by-Step Guide: How to Implement TypeScript Union and Intersection Types
Understanding TypeScript Union Types
A union type in TypeScript is a type that represents one of several possible data types. It allows you to declare a variable, parameter or return type that can be more than one type.
Syntax
type VarName = Type1 | Type2 | Type3 … | TypeN;
Step 1: Define Basic Union Types
Example: Define a variable that can be either string
or number
.
// Define a union type for a variable
let id: string | number = 101;
id = "abc"; // Valid
// id = true; // Invalid -> This will raise an error since boolean is not part of the union
Explanation:
- The
id
variable is allowed to store either astring
or anumber
. - Assigning a
string
ornumber
value toid
works. - Assigning a
boolean
or any other non-matching type raises a compile-time error.
Step 2: Using Union with Functions
Example: Create a function that accepts either a number
or a string
.
function printID(id: number | string) {
console.log("Your ID is: " + id);
}
printID(101); // Output: Your ID is: 101
printID("abc"); // Output: Your ID is: abc
Explanation:
- The
printID
function parameterid
can accept either anumber
or astring
. - Inside the function, both types are treated as the intersection of properties that belong to both types (in this case, only methods/properties common to both
number
andstring
types).
Step 3: Narrowing in Union Types
To make better use of union types, it’s usually necessary to "narrow" them down from their combined set of types to a more specific one within our code. This can be done using typeof
, instanceof
, or custom type guards.
Example: Improve the printID
function by narrowing the type based on typeof
.
function printID(id: number | string) {
if (typeof id === "string") {
// Treat id as string if typeof id === "string"
console.log("Your string ID is: " + id.toUpperCase());
} else {
// Treat id as number in all other cases
console.log("Your numeric ID is: " + id.toFixed(2));
}
}
printID(101); // Output: Your numeric ID is: 101.00
printID("abc"); // Output: Your string ID is: ABC
Explanation:
- The
typeof
operator is used to check the type ofid
. - Within the
if
block, TypeScript understands thatid
is astring
and allows you to use string-specific methods liketoUpperCase
. - In the
else
block, TypeScript infersid
must be anumber
, enabling the use of number-specific methods liketoFixed
.
Understanding TypeScript Intersection Types
An intersection type in TypeScript constructs a type by combining multiple types. It creates an object that has all of the features of the types from which it is intersected.
Syntax
type IntersectedType = Type1 & Type2 & Type3 … & TypeN;
Step 1: Creating Intersected Types
Example: Combine two interface types to form an intersection type.
interface Employee {
employeeID: number;
department: string;
}
interface Manager {
managerID: number;
managedEmployees: Array<Employee>;
}
// Intersection of Employee and Manager
type ManagerEmployee = Employee & Manager;
const managerEmp: ManagerEmployee = {
employeeID: 1,
department: "Technology",
managerID: 2,
managedEmployees: [
{ employeeID: 2, department: "Design" },
{ employeeID: 3, department: "Marketing" }
]
};
console.log(managerEmp);
// Output: { employeeID: 1, department: 'Technology', managerID: 2, managedEmployees: [ { employeeID: 2, department: 'Design' }, { employeeID: 3, department: 'Marketing' } ] }
Explanation:
- The
ManagerEmployee
type is an intersection ofEmployee
andManager
. - A
managerEmp
object must satisfy all property requirements from both interfaces (employeeID
,department
,managerID
, andmanagedEmployees
).
Step 2: Intersection with Multiple Types
Example: Combine multiple types to form an intersection.
type DetailedInfo = {
age: number;
address: string;
} & Employee & Manager;
const detailedPerson: DetailedInfo = {
employeeID: 4,
department: "HR",
managerID: 5,
managedEmployees: [],
age: 30,
address: "123 Elm St"
};
console.log(detailedPerson);
// Output: { employeeID: 4, department: 'HR', managerID: 5, managedEmployees: [], age: 30, address: '123 Elm St' }
Explanation:
- The
DetailedInfo
type is an intersection of an anonymous type (age
andaddress
),Employee
, andManager
. - The
detailedPerson
object must have all properties from each of these types.
Step 3: Combining Primitive Types
Although unions are more common with primitive types, intersections with basic types aren’t typically applicable, they can represent common functionality for objects. However, intersections can be used with utility types or in scenarios where a combination makes logical sense.
Example: Combine two primitive types (unconventional, but shown for completion).
// Unconventional - This doesn't make much sense but shows syntax
type Mixed = number & string;
// No valid assignment possible here as no value satisfies both conditions.
// const mixedVal: Mixed = 42; // Incorrect
// const mixedVal: Mixed = "fourtytwo"; // Incorrect
// Normally, intersections are used for objects or complex logic
Step 4: Using Intersections for Enhancing Functionality
Example: Enhance an existing type with additional properties.
interface BaseUser {
userID: number;
userName: string;
}
interface AuthenticatedUser extends BaseUser & { token: string } {}
const user: AuthenticatedUser = {
userID: 6,
userName: "johndoe",
token: "abcd-efgh-ijkl-mnop"
}
console.log(user);
// Output: { userID: 6, userName: 'johndoe', token: 'abcd-efgh-ijkl-mnop' }
Explanation:
- The
AuthenticatedUser
type extendsBaseUser
and includes an additionaltoken
property via an intersection. - The
user
object of typeAuthenticatedUser
includes all the required properties.
Complete Example with Code
Here is a comprehensive example showcasing both union and intersection types:
// Union Types
interface Dog {
barks: boolean;
numberOfLegs: number;
}
interface Cat {
meows: boolean;
numberOfLegs: number;
}
type Animal = Dog | Cat;
function isDog(animal: Animal): animal is Dog {
return "barks" in animal;
}
// Intersection Types
interface Human {
name: string;
speaksInEnglish: boolean;
}
interface Engineer {
skills: Array<string>;
experienceYears?: number;
}
type EnglishEngineer = Human & Engineer;
const engineer1: EnglishEngineer = {
name: "Jane Doe",
speaksInEnglish: true,
skills: ["TypeScript", "Data Structures"]
};
const engineer2: EnglishEngineer = {
name: "John Smith",
speaksInEnglish: false,
skills: ["Machine Learning", "Deep Learning"],
experienceYears: 5
};
console.log(engineer1);
// Output: { name: 'Jane Doe', speaksInEnglish: true, skills: ['TypeScript', 'Data Structures'] }
console.log(engineer2);
// Output: { name: 'John Smith', speaksInEnglish: false, skills: ['Machine Learning', 'Deep Learning'], experienceYears: 5 }
// Function accepting union type
function animalDetails(animal: Animal) {
console.log(`This animal has ${animal.numberOfLegs} legs`);
if (isDog(animal)) {
console.log(`It ${animal.barks ? "barks" : "does not bark"}`);
} else {
console.log(`It ${animal.meows ? "meows" : "does not meow"}`);
}
}
const myDog: Dog = { barks: true, numberOfLegs: 4 };
const myCat: Cat = { meows: true, numberOfLegs: 4 };
animalDetails(myDog); // Output: This animal has 4 legs \n It barks
animalDetails(myCat); // Output: This animal has 4 legs \n It meows
Explanation:
Union Types:
- Defined
Animal
as a union ofDog
andCat
. AnAnimal
can be either aDog
or aCat
. - Created a type guard
isDog
to determine whether anAnimal
is aDog
, enabling narrowing.
- Defined
Intersection Types:
- Defined
EnglishEngineer
as an intersection combiningHuman
andEngineer
. Represents someone who is both a human and an engineer. - Created examples of engineers who fulfill both the human and engineer criteria with or without optional properties.
- Defined
Top 10 Interview Questions & Answers on TypeScript Union and Intersection Types
1. What are Union Types in TypeScript?
Answer: Union types allow a variable, parameter, or function return type to have multiple possible types. They're useful when a value could be one of several types.
Example:
let value: string | number;
value = "Hello"; // Ok
value = 42; // Ok
2. Could You Provide an Example of How to Use Union Types in Function Parameters?
Answer: Union types can be applied to function parameters to accept different types.
Example:
function printLength(val: string | any[]): number {
return val.length;
}
console.log(printLength("Hello")); // Output: 5
console.log(printLength([1, 2, 3])); // Output: 3
3. How Do You Handle Cases Where You Need to Narrow Down the Type Within a Union?
Answer: In TypeScript, you can use type guards (like typeof
, instanceof
, or custom guards) to narrow down the type within a union.
Example:
function process(input: string | number): void {
if (typeof input === 'string') {
console.log(`String length: ${input.length}`);
} else {
console.log(`Number squared: ${input * input}`);
}
}
4. What Are Intersection Types and How Do They Work?
Answer: Intersection types combine multiple types into one type, combining their properties. It’s used to create complex objects from two or more object types.
Example:
type User = {
id: number;
name: string;
};
type Permissions = {
accessLevel: string;
};
type AdminUser = User & Permissions;
const admin: AdminUser = {
id: 1,
name: "Alice",
accessLevel: "admin"
};
5. Can You Show Me an Example of Intersection Types Involving Interfaces?
Answer: Yes, you can intersect interfaces as well.
Example:
interface Car {
seats: number;
doors: number;
}
interface SportsCar {
horsepower: number;
speed: number;
}
type Supercar = Car & SportsCar;
const supercar: Supercar = {
seats: 2,
doors: 2,
horsepower: 600,
speed: 200
};
6. When Would You Use Union Types Instead of Intersection Types?
Answer: Use union types when a value can be one of several types, but not a combination of all types. Use intersection types when building complex structures which consist of properties from more than one type.
7. Is There Any Limitation with Using Union Types?
Answer: When using union types, TypeScript doesn't allow property access directly unless it's common across all types in the union. This requires type checking or assertion.
Example:
let val: string | number;
// val.toUpperCase(); // Error: Property 'toUpperCase' does not exist on type 'number'
if (typeof val === 'string') {
console.log(val.toUpperCase()); // Now it’s safe!
}
8. Can You Explain Why Union Types Are Useful for Handling Different Event Data in Web Applications?
Answer: Absolutely. Union types help handle events where different event types might share common properties yet differ in other ways.
Example:
type KeyUpEvent = {
type: "keyup";
key: string;
};
type MouseDownEvent = {
type: "mousedown";
button: number;
};
function handleEvent(event: KeyUpEvent | MouseDownEvent) {
if (event.type === "keyup") {
console.log(`Key pressed: ${event.key}`);
} else {
console.log(`Button pressed: ${event.button}`);
}
}
9. How Can I Use Literal Types Alongside Union Types for Better Code Clarity?
Answer: Literal types enhance union types by providing specific values rather than just types, adding clarity to what each union option represents.
Example:
type Direction = "north" | "south" | "east" | "west";
function move(direction: Direction): void {
switch (direction) {
case "north":
console.log("Moving north...");
break;
case "south":
console.log("Moving south...");
break;
case "east":
console.log("Moving east...");
break;
case "west":
console.log("Moving west...");
break;
default:
console.error("Unknown direction!");
}
}
10. Can You Illustrate a Real-world Scenario where Both Union and Intersection Types Are Used Together?
Answer: Imagine you have different types of employees (FullTime
, PartTime
), each with their own set of properties. Yet, every employee has some common properties like name
and id
. You can use union and intersection types together for this.
Example:
interface EmployeeBase {
id: number;
name: string;
}
interface FullTimeEmployee extends EmployeeBase {
salary: number;
benefits: string[];
}
interface PartTimeEmployee extends EmployeeBase {
hourlyRate: number;
hoursPerWeek: number;
}
type Employee = FullTimeEmployee | PartTimeEmployee;
const employee: Employee = {
id: 1,
name: "John Doe",
salary: 50000,
benefits: ["healthInsurance", "retirement"]
};
function describeEmployee(employee: Employee): void {
console.log(`Employee ID: ${employee.id}, Name: ${employee.name}`);
if ("salary" in employee) {
console.log(`Full-time with Salary: $${employee.salary}`);
} else {
console.log(`Part-time with Hourly rate: $$${employee.hourlyRate}`);
}
}
describeEmployee(employee);
In this scenario:
EmployeeBase
provides common properties.FullTimeEmployee
andPartTimeEmployee
extendEmployeeBase
with unique properties.Employee
is a union ofFullTimeEmployee
orPartTimeEmployee
.- The function
describeEmployee()
handles both cases using a type guard (in
operator) to identify the correct employee type and access the appropriate properties.
Login to post a comment.