Enforcing 'noImplicitAny' on callbacks to generic functions
Tags: typescript
Published: 2024-06-10 20:38:24

One of the first errors every TypeScript developer encounters is the famous `noImplicitAny`, which happens when you don't specify a type for a variable. For example:
```ts
function fn(s) {
  //        ^? Parameter 's' implicitly has an 'any' type.
  console.log(s.subtr(3));
}
```

This is a great thing because it forces you to think about the types of your variables and functions, helping you avoid bugs.

## The problem

When developing your own functions that receive a callback, you might lose that error checking if you specify the type of the callback as `(...args: any[]) => any`. This type indicates that the function can receive any number of arguments of any type.

```ts
// @ts-expect-error
function fn(a) {}
const wrapper = (f: (...args: any[]) => any) => f
const test = wrapper((a) => {})
//                    ^? any
// TS won't complain
```

If you don't know, `(...args: any[]) => any` is one of the most popular ways to type a function. The `Function` primitive is not recommended because it can accept anything that is callable, such as a constructor, a callable object, or even a non-callable class such as `Map`.
But this use case creates a bad developer experience because you're losing one of the basic benefits of the type system. An `any` can easily go unnoticed and cause bugs further down the line.

## The quest

Last week, I started a [Twitter thread](https://twitter.com/gugaguichard/status/1798802769996136459) about this and asked for help from some friends and TS experts.
I was working on our library [composable-functions](https://github.com/seasonedcc/composable-functions) on an [issue by @danielweinmann](https://github.com/seasonedcc/composable-functions/issues/146) that was experiencing this problem.

[The problem](https://tsplay.dev/NVX0xW) seemed impossible and the solutions would require some trade-offs.
- Forbidding the use of `any` in the callback's arguments was reasonable since the library I was working on is for strict TypeScript projects and `any` is probably out of the question. However, it would be a breaking change for the users and quite strict.
- Swapping `(...args: any[]) => any` for `(...args: never[]) => unknown`. This was suggested by our contributor @jly36963 which is a thread on the [TS repo itself](https://github.com/microsoft/TypeScript/issues/37595#issuecomment-604103234). It seems nice because if you don't declare the types, the arguments are gonna be `never`, so you can't use them within the function. There was one concern though, this solutions causes problems with default arguments - as you can read in the last comment on that thread.

This is when I decided to go bold and try that `Function` primitive!

## The solution

The solution was to use it in the function parameter but restrict it anywhere else down the line. We only need to enforce `noImplicitAny` in the declaration of the callback, but we have other ways to enforce a narrower type, such as `(...args: any[]) => any`, elsewhere.

The diff was quite simple:
```diff
-function composable<T extends (...args: any[]) => any>(fn: T): Composable<T> {
+function composable<T extends Function>(
+  fn: T,
+): Composable<T extends (...args: any[]) => any ? T : never> {
```

And it worked! The error was back and the user would have to specify the types of the arguments of the callback.

If the user tried to use something which isn't an actual function, they would get a `Composable<never>` which is impossible to work with.

You can check the [final PR here](https://github.com/seasonedcc/composable-functions/pull/147)

## Conclusion
There were two motivations to write this post:
- I couldn't find any information about this on the internet.
- I love simple solutions that require you to think outside the box and break some rule as long as you cover up for them.

I hope this post can help someone else who is struggling with the same problem.