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.


Let us help you on your journey to Quality Faster

We at Xolvio specialize in helping our clients get more for less. We can get you to the holy grail of continuous deployment where every commit can go to production — and yes, even for large enterprises.

Feel free to schedule a call or send us a message below to see how we can help.

User icon
Envelope icon

or

Book a call
+
Loading Calendly widget...
  • Add types to your AWS lambda handler

    Lambdas handlers can be invoked with many different, but always complex, event arguments. Add to that the context, callback, matching return type and you basically start listing all the different ways that your function can fail in production.

  • How to expose a local service to the internet

    From time to time you might need to expose your locally running service to the external world - for example you might want to test a webhook that calls your service. To speed up the test/development feedback loop it would be great to be able to point that webhook to your local machine.

  • For loops in JavaScript (vs _.times)

    From time to time I still see a for loop in JavaScript codebases. Linters are frequently angry about them. Let's see how we can replace them.