DEV Community

yw662
yw662

Posted on

A "function type covariance" trap in typescript

In JavaScript and TypeScript, functions are generic, which means a:

type f = (...args: [number]) => unknown
// aka: (foo: number) => unknown
Enter fullscreen mode Exit fullscreen mode

is automatically a

type f = (...args: [number, ...any[]]) => unknown
Enter fullscreen mode Exit fullscreen mode

Reasonable. If a function uses only the first few arguments, it is no harm to provide more.

And here come "optional parameters" in TypeScript. No worry in JavaScript since there are no "non-optional parameters":

type g = (foo: number, bar?: number) => unknown
Enter fullscreen mode Exit fullscreen mode

It is also a:

(foo: number) => unknown
Enter fullscreen mode Exit fullscreen mode

Why not? the second parameter is optional, it can be used like that.

So now, a g is also an f.
But wait, remember we have the second form of f:

const H = (h: (foo: number, bar: string) => void) => {
  h(0, '')
}

const F = (f: (foo: number) => void) => {
  H(f)
}

const g = (foo: number, bar?: number) => {
  console.log(bar ?? 0 + foo + 1)
}

F(g)
Enter fullscreen mode Exit fullscreen mode

TypeScript would gladly accept these code even in its most strict type checks, including strictFunctionTypes: a g is an f, we already know that, and an f is an h, we know that too. But is a g also an h ?

That is the question.

We have been using a lot of functional APIs. Array.prototype.map for example, accepts an executor (element, index?, array?) => any, which is practically an element => any.
But if the executor is from somewhere else in the later form, the "g is not h" can be a problem, a problem TypeScript unable to detect:

class Foo<T> {
  private foo: T[]
  ...
  function bar<U>(f: T => U) {
    return this.foo.map(f)
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

Let's imagine what could happen here.

Top comments (0)