A "function type covariance" trap in typescript
yw662
Posted on February 12, 2022
In JavaScript and TypeScript, functions are generic, which means a:
type f = (...args: [number]) => unknown
// aka: (foo: number) => unknown
is automatically a
type f = (...args: [number, ...any[]]) => unknown
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
It is also a:
(foo: number) => unknown
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)
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)
}
...
}
Let's imagine what could happen here.
Posted on February 12, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 28, 2024