How to get rid of the last any in your TypeScript Code

Lukas Gamper
Towards Dev
Published in
3 min readDec 15, 2021

--

Photo by Markus Spiske on Unsplash

TypeScript offers an impressive set of tools to manipulate types. Using these tools we can tighten the safety net of strong types and catch any syntax error during compilation.

But there are some functions which are hard to type strongly. Like the first function below. It returns the first element of an array or the argument itself, if a no array type is passed. If a multidimensional array is passed, call the function recursively until a non array type is reached.

Let’s develop an ElementType<T> type declaration which allows us to type the first function strongly.

Strait Forward Type Declaration

In TypeScript we can use an indexed access type to get the type of a property of another type. For example the type declarationtype AgeType = Person[“age”] evaluates as the type of the age property.

In our use we want to access the element type of the first element in an array. But since every element of an array has the same type, we can access the element type with Arr[number].

Now we have everything to create a generic declaration to get the element type of an array.

But TypeScript does not like our first attempt. T can be any type, so typescript raises an error, that a generic type T cannot be used as an indexable type. We need to explain to TypeScript that T is an indexable type. The can be achieved using constraint types.

Constrained Types

A type variable can be constrained using the extends keyword. This means the type declaration is only valid if the type variable can be assigned to the right side of the extends keyword.

To make sure our generic type T is indexable we require it to be assignable to Array<any>.

Now TypeScript knows, that T is an array type and we can extract the element type using the indexed access type. But what happen if we pass a scalar, an object or any non array type to ElementType?

TypeScript complains, that number is not an array type. But we want any non array type to return itself. For example ElementType<number> should evaluate to number. Conditional types are the perfect tool for this.

Conditional Types

A conditional type has the form AType extends AnotherType ? TypeIfAssignable : TypeElse. If AType is assignable to AnotherType the type of the first branch is returned else the type of the second branch is returned.

Using a conditional type we can implement a different behaviour if an array type is passed to ElementType<T> or if a non array type is passed. With a conditional type our ElementType<T> declaration can be expressed as follows:

We’re almost there, we can already determine the element type of an array or return the type itself if it’s a non array type. But TypeScript offers more tools to simplify our type declaration.

Inferring within Conditional Types

Conditional types can infer types form the right side of the extends keywords. The infer keyword declares a new generic type variable instead of specifying how to retrieve the type. The infer keyword can extract many type aliases like argument or return types from function types.

Using the infer keyword, we simplify our type declaration:

The infer keyword declares the generic type variable ElT instead of specifying how to retrieve the element type using indexed access.

But what happens if we want to extract the element type of a multidimensional array?

The ElementType<T> is not recursive, it only extracts the element type of the first array instead of recursively extracting the element type until we reach a non array type. Since TypeScript 3.7 we can declare recursive type declarations:

That’s it! We managed to declare an ElementType<T> type wich can extract the element type of any array, even if it’s nested.

Let’s look at our first function for where we started. Now we can strongly type the function.

Conclusion

Using conditional types with type inference we can strongly type complex functions. Removing any types from your code helps to increase the code quality and catches runtime errors already during compile time.

In most cases we don’t have to create our own utility types. TypeScript offers many utility types to handle common type transformations.

--

--