January 15, 2021

How to deal with inferred TypeScript types and stop using any

What do you do when you are in need to figure out a complex type?
I see frequently in many codebases that people just give up - they either make use of any or jump through a lot of hoops that in the end makes their code less maintainable.. but at least they didn't have to figure out the types, right? :)

I can't blame them, sometimes it seems that the extra time invested in some of those edge-case problems with TypeScript, especially coming from JavaScript background seems like an unnecessary slow-down.

Let's talk about one of those situations.
Assume we deal with a function that returns a rather tangled shape. Keep in mind - in the real world, this could be hundreds of lines long/much more complex. Maybe even the shape would depend on arguments passed. So our example is hardly close to the bad ones that haunt the developers, in the end.

const functionReturningComplexShape = (userId: String) => ({
   some: {
     deeply: {
       nested: {
         array: ["apple", "potato"],
       },
     },
   },
   isItTrue: userId === 'number one',
})

We use it in a different function and do some transformation on it:

const user = {id: 'userId'}
const getUserId = u => user.id

const consumerFunction = () => {
 const userId = getUserId(user)
 const val = functionReturningComplexShape(userId);  
 const parsedValue = {  
 numberOfElements: val.some.deeply.nested.array.length,  
 fruitsInTheList: val.some.deeply.nested.array.find((f) => f === "apple"),
 wasItTrue: val.isItTrue
 };
 // 10 more lines of code.
}

When we do everything in-line like here, it's all nice and clean. It's actually just pure JavaScript!
We would like to make the parsing logic testable, or maybe we need it somewhere else as well. Imagine if we do this parsing in a loop, or maybe the functionReturningComplexShape function depends on more arguments that have to be obtained first. Testing it through the consumerFunction will be painful, and the tests will probably have to be changed every time that consumerFunction is changed, even though the parsing logic might remain unchanged!
It seems only natural that we want to extract the logic.
But here the problems start.

const parseIt = (val: any) => ({  
 numberOfElements: val.some.deeply.nested.array.length,  
 fruitsInTheList: val.some.deeply.nested.array.find((f) => f === "apple"),
 wasItTrue: val.isItTrue
})

const consumerFunction = () => {
 const userId = getUserId(user)
 const val = functionReturningComplexShape(userId);  
 const parsedValue = parseIt(val)
}

The original function return type is inferred by TypeScript, which is great. But now, we don't have a way to use a defined type to reuse it in our parseIt function. As I said - many people, after a few frustrated grunts, would just put any and be done with it.
But we can do better!
TypeScript gives us ReturnType helper, let's try it:

// remember to put the `typeof` before the function name.
const parseIt = (val: ReturnType<typeof functionReturningComplexShape>) => ({  
 numberOfElements: val.some.deeply.nested.array.length,  
 fruitsInTheList: val.some.deeply.nested.array.find((f) => f === "apple"),
 wasItTrue: val.isItTrue
})

const consumerFunction = () => {
 const userId = getUserId(user)
 const val = functionReturningComplexShape(userId);  
 const parsedValue = parseIt(val)
}

Nice!
But, frequently those complex states come from async calls, right? It might be a database, some inconsiderate 3rd party API... What happens if our original function was async? Let's see

const functionReturningComplexShape = async (userId: String) => ({
   some: {
     deeply: {
       nested: {
         array: ["apple", "potato"],
       },
     },
   },
   isItTrue: userId === 'number one',
})

const parseIt = (val: ReturnType<typeof functionReturningComplexShape>) => ({  
 numberOfElements: val.some.deeply.nested.array.length,  
 fruitsInTheList: val.some.deeply.nested.array.find((f) => f === "apple"),
 wasItTrue: val.isItTrue
})

const consumerFunction = async () => {
 const userId = getUserId(user)
 const val = await functionReturningComplexShape(userId);  
 const parsedValue = parseIt(val)
}

Well... It's not pretty. The return type of the function is now hiding behind the promise. TypeScript is not so helpful anymore but there are nice and smart people out there and they figured it out.
https://github.com/piotrwitek/utility-types has a bunch of nice utility types that might come very handy, we could use PromiseType from it to construct ReturnPromiseType:

import { PromiseType } from 'utility-types';

type PromiseReturnType<T extends (...args: any) => Promise<any>> = PromiseType<ReturnType<T>>;

Let's give it a try:

const parseIt = (val: PromiseReturnType<typeof functionReturningComplexShape>) => ({  
 numberOfElements: val.some.deeply.nested.array.length,  
 fruitsInTheList: val.some.deeply.nested.array.find((f) => f === "apple"),
 wasItTrue: val.isItTrue
})

Cool. That worked.
If we don't want to install the whole package, you can "steal" just that one utility:

type PromiseType<T extends Promise<any>> = T extends Promise<infer U>  
 ? U  
 : never;
type PromiseReturnType<T extends (...args: any) => Promise<any>> = PromiseType<ReturnType<T>>;

Explaining how that works is beyond the scope of this article - you don't need to understand to be able to nicely use it. If PromiseReturnType was defined by TypeScript, you might happily live without looking under the hood, remember the ReturnType I introduced earlier? I didn't hear you asking questions and being suspicious ;-)

Nonetheless, if you want to go deeper, take a look at this article, https://www.jpwilliams.dev/how-to-unpack-the-return-type-of-a-promise-in-typescript , but don't say I didn't warn you!

Enjoy :-)

Let me know if you have any questions or thoughts in the comments below.

Keep reading