Thursday, January 11, 2024 · 5 min read
typescript
javascript
frontend

Advanced Union Type Techniques in TypeScript — 03 Indexed Types

Leverage Indexed Types for Enhanced Type Safety and Code Clarity

Substack Banner TS101.png

Welcome back to the “Advanced Union Type Techniques in TypeScript” series. Previously, we looked into how Extract and Exclude utility types refine our type handling in TypeScript. In this post, we're exploring another essential aspect of TypeScript's type system: Indexed Types.

Exploring Indexed Types in TypeScript…

Indexed Types in TypeScript represent a significant shift in how we approach types. This feature allows us to use the dynamic nature of JavaScript while retaining the type safety TypeScript is renowned for. It's about querying and manipulating types in a way that’s analogous to handling values, which is particularly powerful for complex data structures.

For instance, consider a UI component's configuration object, which includes settings like width, height, and color. TypeScript enables us to extract the types of these properties directly using their keys:

type ComponentConfig = {  
 width: number;  
 height: number;  
 color: string;  
};  
  
type WidthType = ComponentConfig['width']; // number  
type ColorType = ComponentConfig['color']; // string

Here, WidthType and ColorType directly pull out the types of width and color from ComponentConfig. This ability is immensely useful in creating functions that adapt to any property of ComponentConfig, returning the corresponding type.

Let’s take it to a real-world level. You’re tasked with writing a function that dynamically fetches values based on property names from a user’s profile object. The profile has various attributes like nameemail, and age:

type UserProfile = {  
 name: string;  
 email: string;  
 age: number;  
};  
  
type ProfileProperty = keyof UserProfile;  
type ProfileValue<T extends ProfileProperty> = UserProfile[T];  
  
const userProfile: UserProfile = {  
  name: 'John Doe',  
  email: 'jd@example.com',  
  age: 30  
 }  
  
function getProfileValue<T extends ProfileProperty>(prop: T): ProfileValue<T> {  
 // fetch logic here  
 return userProfile[prop];  
}  
  
// Correct usage  
// Without TypeScript type error  
const userName = getProfileValue('name'); // 'John Doe'  
  
// Incorrect usage  
// With TypeScript type error  
const userPhoneNumber = getProfileValue('phone') // TypeScript Type Error: Argument of type '"phone"' is not assignable to parameter of type 'keyof UserProfile'.

With keyof and indexed access types, getProfileValue is a generic function that can now safely return the type of any property from UserProfile.

This technique doesn’t just apply to single properties. It extends to arrays and other complex structures, allowing for the extraction of deep types within nested objects or arrays, enabling strong typing across even the most complex data structures.

Consider a scenario where you are working with a complex data structure representing a company’s organizational chart. The chart includes departments, and each department has a list of employees:

type Employee = {  
  id: number;  
  name: string;  
  role: string;  
};  
  
type Department = {  
  name: string;  
  manager: Employee;  
  employees: Employee[];  
};  
  
type Company = {  
  name: string;  
  departments: Department[];  
};

Now, suppose you need to work with the type of the employees array within a department. Using indexed types, you can extract this type directly from the Department type:

type EmployeeArrayType = Department['employees']; // Employee[]  
  
function processEmployeeList(employees: EmployeeArrayType) {  
  employees.forEach(employee => {  
    console.log(`Employee ID: ${employee.id}, Name: ${employee.name}, Role: ${employee.role}`);  
  });  
}  
  
const someDepartment: Department = {  
  name: "Development",  
  manager: { id: 1, name: "Jane Doe", role: "Department Manager" },  
  employees: [  
    { id: 2, name: "John Smith", role: "Developer" },  
    { id: 3, name: "Alice Johnson", role: "Developer" },  
  ],  
};  
  
// Correct usage  
// Without TypeScript type error  
processEmployeeList(someDepartment.employees);  
  
// Incorrect usage  
// With TypeScript Type Error  
const incorrectData = { id: 4, name: "Eve Adams", role: "Intern" };  
processEmployeeList(incorrectData); // TypeScript Type Error: Argument of type '{ id: number; name: string; role: string; }' is not assignable to parameter of type 'Employee[]'.

In this example, EmployeeArrayType is an alias for the type of employees property in a Department, which is Employee[]. This technique allows you to directly use the extracted type in functions or other parts of your code, ensuring consistency and leveraging TypeScript's type checking capabilities for complex nested structures.

Indexed types are not just a feature of TypeScript; they’re a paradigm that, when leveraged correctly, can mirror the dynamic nature of JavaScript while maintaining the type safety that TypeScript is known for.

Closing Thoughts

In this post, we've looked into Indexed Types, a pivotal aspect of TypeScript's type system. This feature enhances our ability to handle complex data structures with precision and clarity. From simple configurations to intricate organizational charts, Indexed Types provide a level of depth and flexibility in how we handle types in TypeScript.

By aligning the dynamic capabilities of JavaScript with TypeScript's robust type safety, Indexed Types offer a paradigm that can efficiently handle even the most complex data structures. This not only improves the reliability of our code but also its readability and maintainability.

Stay tuned as we continue to explore more advanced features in TypeScript. These insights will further expand our understanding and capabilities in this powerful language.

Thank you for following along in this series. I'm excited to share more in the upcoming posts.

Until next time 👋!