Mar 27, 2025

Atomic functions

Often, we see functions that require an entire object to be passed in, even if only 1 or 2 attributes from that object are used. For example:

ts
1function getProductImageFromCart(product: Product): string { 2 const { imageUrl } = useProducts(); 3 const image = getImageWithWidth(product, 64); 4 return getImageUrl(image, product.name) || fallbackUrl; 5}

This approach can make functions rigid and less flexible, especially when dealing with partial objects or different types of objects. For instance, a MiniProduct might have all the necessary data but cannot be used because it is not specifically a Product.

A better approach is to create atomic functions that only take the data they need. However, making this change can be time-consuming. An alternative solution is to use a functional type like this:

ts
1export type RequireSome<T, K extends keyof T> = Partial<T> & { 2 [P in K]-?: T[P]; 3};

This type creates a partial version of the type you want to use and makes the specified attributes required.

ts
1function getProductImageFromCart( 2 product: RequireSome<Product & MiniProduct, "name" | "images"> 3): string;

This allows us to pass any type that includes the required attributes, ignoring the rest without causing errors. It also enables the use of well-typed objects with only the necessary data, making the function atomic.

So, we can use it like this:

ts
1getProductImageFromCart({ name: product.name, images: product.images });

This approach provides an atomic structure and dynamic type safety. As you address these TypeScript issues, consider using this method to make functions more flexible and maintainable.