Jul 18, 2024

Atomic functions

I see in many functions where we use only 1 or 2 attributes from an object, we still require the entire object gets passed, like this: 

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

Ideally, we shouldn’t be doing this because it makes our functions really rigid, and not usable for cases where we have partial products, or different real types of products. In this case a MiniProduct has all the data needed, but isn’t usable because it isn’t specifically a Product.

The best way to go about fixing this is to make atomic functions where we only take the data we need. But, making this change could take a lot of effort. There is another way though, which is by using a functional type like this:

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

This makes a partial of the type you want to use, then makes all the listed attributes required.

ts
1function getProductImageFromCart(productRequireSome<Product & MiniProduct'name' | 'images'>): string

This allows us to pass any type we want, as long as it has the two things we need, and will discard the rest but not error out if you pass something with a different form. This also allows you to use well-typed objects with only the data we need, making it atomic. 

So, we can use it like this:

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

And this works great, giving us atomic structure and dynamic typesafety. So, as you’re fixing these TS issues please use this approach to atomize our functions where needed.