When we work with any(😬) amount of TypeScript code, chances are we will
encounter the any
keyword. Most of the uses we have seen indicate that we are
dealing with the base type in TypeScript land. Ruby’s is Object
same for
C#, inside the documentation we find:
(…) values from code that has been written without TypeScript or a 3rd party library. In these cases, we might want to opt-out of type checking. To do so, we label these values with the any type:
What is any
So it is not a wildcard
, and it is not the base type; it is explicitly to
interact with 3rd party libraries. Then why is it around so often? Is it
detrimental to our systems? Should we run from it or embrace it?
The any type is a powerful way to work with existing JavaScript, allowing you to gradually opt-in and opt-out of type checking during compilation.
Let that sink in. The TypeScript documentation express
clearly that when we have the any
type, we are telling the compiler:
We are saying no thank you
when over 500 contributors to the language offer
their help. It sounds like to opt-out of the type checker, and with it, losing
all security and confidence in our type system should not be a decision taken
lightly. We should be using it to interact with non-typed 3rd party(or 1st
party) Javascript code, or when we only know some part of the type.
But wait I have a lot of other reasons
Isn’t TypeScript transpiling to Javascript? Isn’t Javascript dynamic? Why should I take care of my types then?
Yes! But we are writing our code in TypeScript, which is a statically typed
language. One can argue that statically typed languages don’t result in fewer
bugs than dynamic languages. Still, in a statically typed language where we use
something like any
, that’s the worst of both worlds.
Some things are hard to type correctly, and any
is easier
It is easy to indulge the lazy developer in us. If we don’t type it correctly, we’re going to write bugs, more bugs than we would in a dynamic language because we force TypeScript, a statically typed language, to go without checking for incorrect types.
I really don’t know what it is
That’s fine! We can use unknown
; it allows us to assign any type indeed.
But we won’t be allowed to operate with the values until a specific
type is determined.
type ParsedType = {
id: number
}
const parseApiResponse(
response: Record<string, unknown>
): ParsedType => {
const convertedResponse = (response as ParsedType)
// without doing the type cast we would
// get a type error here
if(convertedResponse.id >= 0) {
return convertedResponse
} else {
throw Error.new("Invalid response"
}
}
I have to write a lot of code when I add types, any
is less work
It probably isn’t; if we are writing code without types, we will likely add
defensive code to make sure arguments and variables have the correct shape for
the program to perform as intended. any
can’t even guard our logic against
null
or undefined
checks.
// version 1 with `any`
const fullName = (user: any) => {
if (user?.firstName && user?.lastName) {
return `${user.lastName}, ${user.firstName}`
}
return user?.firstName || ""
}
// version 1 without `any`
interface User {
firstName: string
lastName?: string
}
const fullName = ({ firstName, lastName }: User) => {
if (lastName === undefined) {
return firstName
}
return `${lastName}, ${firstName}`;
}
Types add so much complexity, sometimes any
is simpler
Using any
might allow us to make progress without putting too much thought
into how the data flows into our logic. But it shifts that burden to future
readers of our code. They will have to interpret what is happening without the
context we had and without help from the compiler.
With documentation I can provide all the context
When we add the types, we are getting help from the compiler, and we are getting documentation that will not decay with time, because our code won’t compile if it is outdated.
const intersection = (a: any, b: any): any => {
...
}
const intersection = (
a: Set<number>, b: Set<number>
): Set<number> => {
...
}
They are both equivalent, but the reader will have a better idea of what the last function is doing, not so much from the first one.
But I’ve written the code in a defensive way with the necessary runtime checks to ensure there isn’t an error
There might not be an error now, but unless you have excellent test coverage,
someone coming in to change that code later can’t have confidence that they’re
not refactoring in a bug; it’s almost like the compiler won’t help you because
we said it not to. If we explicitly set the types and change an API
consumed
in our system, the compiler will offer its guide.
What if I later change my mind about the type? Having it all explicit will have me refactoring for hours.
We can always modify and accommodate new type definitions. TypeScript offers
an array of utilities for this purpose. We can use Pick
to, well, pick the
properties we need from a previously defined type. Omit
to get everything but
a handful of them. Partial
to make all attributes optional or do a full 180
and make them all Required
.
type User = {
id: number;
firstName: string;
lastName: string;
age: number;
}
type UserParams =
Pick<User, "id"> & Partial<Omit<User, "id">>
const updateUser = (
{ id, ...newUserParams }: UserParams
) => {
{...}
}
Fine, deleting any from TypeScript, opening a PR right now
Let’s take a deep breath, any
is extremely powerful and useful in the right
scenario.
Interfacing with libraries that use it; make sure we turn it into the right type as soon as possible before moving that data through the system.
Getting around TypeScript type bugs; if we find ourselves in a situation where it’s impossible to type something,
any
might be necessary. But only resort to this after trying every other approach. And if we use it, we should turn it back into a predictable type ASAP.If our function can genuinely handle any type, this is rare and situational (such as a debug or logging function maybe?) In these cases, we need to be 100% sure there is no type in existence that will cause the function to fail. We should to examine the body of the function and determine the most basic shape from the input and restrict it. For example, if we want to print something, we should, at a minimum, verify that it responds to
toString
. Small steps.
Let’s recap:
Why shouldn’t we use any again?
- It yields the compiler obsolete:
- We’re telling the compiler
Help not needed, I got this
- We’re telling the compiler
- We’re passing on an opportunity to document the code as we write it
- Our first line of defense is down
- In a dynamic language, we assume that things can have any type, and the patterns we employ follow this assumption. If we start using a statically typed language as a dynamic one, then we’re fighting the paradigm.
- As we continue to make changes to the codebase, there’s nothing to guide/help us.
- With great freedom comes great responsibility (the compilers). Don’t turn into a compiler, use one.