Introduction
Configurable objects are a common pattern in software development, allowing functions and APIs to be customized based on various parameters. However, ensuring that certain parameters are present or absent based on a certain condition can be a challenge. Consider the example of a function that generates SQL queries based on configurable search parameters. Some search parameters may be required for advanced search queries, but not for basic search queries. Without proper type-checking, it's easy to accidentally omit or include required parameters, leading to bugs and maintenance issues down the line.
Fortunately, TypeScript's advanced type system offers a powerful solution to this problem. By leveraging conditional types, mapped types, and intersection types, we can create type, which enforces parameter requirements at compile time based on a certain condition. We can define configurable objects that are both flexible and type-safe, making our code more reliable and easier to maintain. In this article, we'll explore how to use TypeScript's advanced types, and demonstrate its benefits through real-world examples.
TypeScript Advanced Types Overview
Brief overview of the different advanced types in TypeScript (e.g. mapped types, intersection types).
TypeScript's advanced types are a set of features that extend the type system beyond basic types such as strings, numbers, and booleans. These advanced types offer a more powerful way of describing complex types and defining relationships between them.
There are several advanced types in TypeScript, including:
Mapped Types: A type that transforms the properties of an existing type by mapping them to new properties. For example, a type that makes all properties of an existing type optional, or a type that adds a prefix or suffix to all property names.
Intersection Types: A type that combines two or more types into a single type, where the resulting type includes all properties and methods from each of the input types.
These advanced types can be used together to create complex and flexible types that are adapted to specific use cases.
Creating the type
Step-by-step guide to creating the type mapped types and intersection types.
Let's consider the code below. We'll create the type and name this TConditionalParams
To create TConditionalParams
, we'll need to use a combination of mapped types, intersection types. Here's a step-by-step guide to creating the type:
- Define the
ISomeParams
interface, which represents a set of optional parameters:
interface ISomeParams {
param1: string;
param2: number;
param3: string;
}
- Define the
IAlwaysRequiredParams
interface, which represents a set of required parameters:
interface IAlwaysRequiredParams {
requiredParam: number;
}
- Define the
TWithCheck
type, which represents a set of parameters that requireISomeParams
andIAlwaysRequiredParams
to be present:
type TWithCheck = {
checkParams: true;
} & IAlwaysRequiredParams & ISomeParams;
- Define the
TWithoutCheck
type, which represents a set of parameters that only requireIAlwaysRequiredParams
to be present.
type TWithoutCheck = {
checkParams: false;
} & IAlwaysRequiredParams;
Finally, define the TConditionalParams
type as a union of TWithCheck
and TWithoutCheck
:
export type TConditionalParams =
| TWithCheck
| TWithoutCheck;
With this type definition, we can now create type-safe functions and APIs that allow for configurable objects with parameter requirements that are enforced at compile time. In the next section, we'll explore some examples of using the type in practice.
Explanation of how the type enforces parameter requirements at compile time based on a condition.
The condition in TConditionalParams
is defined by the checkParams property, which is a boolean that determines whether to enforce additional parameters. When checkParams
is true, the object must include all properties from both IAlwaysRequiredParams
and ISomeParams
. When checkParams
is false, the object must include IAlwaysRequiredParams
.
Read world example
Types definition:
interface Product {
price: number;
category: string;
rating: number;
name: string;
id: number;
}
interface IAdvancesSearchParams {
price: number;
category: string;
rating: number;
}
interface ISearchParams {
keywords: string;
}
type TAdvancedSearch = {
useAdvancedSearch: true;
} & IAdvancesSearchParams &
ISearchParams;
type TBasicSearch = {
useAdvancedSearch: false;
} & ISearchParams;
type TProductSearchParams = TBasicSearch | TAdvancedSearch;
Function declaration:
function searchProducts(products: Array<Product>, params: TProductSearchParams): Array<Product> {
const matchingProducts = products.filter(product => product.name.includes(params.keywords));
if (params.useAdvancedSearch) {
if (params.price === undefined || params.category === undefined || params.rating === undefined) {
throw new Error('Missing required parameters!');
}
return matchingProducts.filter(
product =>
product.price <= params.price && product.category === params.category && product.rating >= params.rating
);
}
return matchingProducts;
}
Code usage:
// Valid usage: useAdvancedSearch = true
const advancedResults = searchProducts([], {
useAdvancedSearch: true,
keywords: 'organic',
price: 20,
category: 'produce',
rating: 4.5,
});
// Returns an array of products that match the advanced search criteria
// Valid usage: useAdvancedSearch = false
const basicResults = searchProducts([], {
useAdvancedSearch: false,
keywords: 'organic',
});
// Returns an array of products that match the basic search criteria
// Invalid usage: useAdvancedSearch = false
const invalidResults = searchProducts([], {
useAdvancedSearch: false,
keywords: 'organic',
price: 20,
});
// Complitation error Object literal may only specify known properties, and 'price' does not exist in type 'TBasicSearch'.
// Invalid usage: useAdvancedSearch = true
const invalidResultsWithAdvancedSearch = searchProducts([], {
useAdvancedSearch: true,
keywords: 'organic',
price: 20,
category: 'produce',
});
// Compilation error Property 'rating' is missing in type '{ useAdvancedSearch: true; keywords: string; price: number; category: string; }' but required in type 'IAdvancesSearchParams'.
Conclusion
In this article, we've explored how to use TypeScript's advanced types to create the type that enforces parameter requirements at compile time based on a certain condition. With this type, we can define configurable objects that are both flexible and type-safe, improving the reliability and maintainability of our code.
By leveraging mapped types and intersection types, we can create complex and flexible types that are adapted to specific use cases. Using these types, we can enforce parameter requirements at compile time, preventing runtime errors caused by missing or extra parameters. This makes our code more reliable and easier to maintain, especially as our codebase grows in complexity.
Top comments (0)