Skip to content

Commit

Permalink
Recover missing FAQ section on functions/signatures with fewer parame…
Browse files Browse the repository at this point in the history
…ters being assignable to those with more parameters.
  • Loading branch information
DanielRosenwasser committed May 24, 2024
1 parent fc10f93 commit ec64999
Showing 1 changed file with 66 additions and 1 deletion.
67 changes: 66 additions & 1 deletion FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -797,7 +797,8 @@ Many questions in this FAQ boil down to "because types are erased".

### Why are function parameters bivariant?

> I wrote some code like this and expected an error:
> I wrote some code like this and expected an error:\
>
> ```ts
> function trainDog(d: Dog) { ... }
> function cloneAnimal(source: Animal, done: (result: Animal) => void): void { ... }
Expand Down Expand Up @@ -849,6 +850,70 @@ so we have to take a correctness trade-off for the specific case of function arg

### Why are functions with fewer parameters assignable to functions that take more parameters?

> I wrote some code like this and expected an error:
>
> ```ts
> function handler(arg: string) {
> // ....
> }
>
> function doSomething(callback: (arg1: string, arg2: number) => void) {
> callback('hello', 42);
> }
>
> // Expected error because 'doSomething' wants a callback of
> // 2 parameters, but 'handler' only accepts 1
> doSomething(handler);
> ```
This is the expected and desired behavior.
First, refer to the "substitutability" primer at the top of the FAQ -- `handler` is a valid argument for `callback` because it can safely ignore extra parameters.

Second, let's consider another case:
```ts
let items = [1, 2, 3];
items.forEach(arg => console.log(arg));
```

This is isomorphic to the example that "wanted" an error.
At runtime, `forEach` invokes the given callback with three arguments (value, index, array), but most of the time the callback only uses one or two of the arguments.
This is a very common JavaScript pattern and it would be burdensome to have to explicitly declare unused parameters.

> But `forEach` should just mark its parameters as optional!
> e.g. `forEach(callback: (element?: T, index?: number, array?: T[]))`
This is *not* what an optional callback parameter means.
Function signatures are always read from the *caller's* perspective.
If `forEach` declared that its callback parameters were optional, the meaning of that is "`forEach` **might call the callback with 0 arguments**".

The meaning of an optional callback parameter is *this*:
```ts
// Invoke the provided function with 0 or 1 argument
function maybeCallWithArg(callback: (x?: number) => void) {
if (Math.random() > 0.5) {
callback();
} else {
callback(42);
}
}
```

`forEach` *always* provides all three arguments to its callback.
You don't have to check for the `index` argument to be `undefined` - it's always there; it's not optional.

There is currently not a way in TypeScript to indicate that a callback parameter *must* be present.
Note that this sort of enforcement wouldn't ever directly fix a bug.
In other words, in a hypothetical world where `forEach` callbacks were required to accept a minimum of one argument, you'd have this code:

```ts
[1, 2, 3].forEach(() => console.log("just counting"));
// ~~ Error, not enough arguments?
```
which would be "fixed", but *not made any more correct*, by adding a parameter:
```ts
[1, 2, 3].forEach(x => console.log("just counting"));
// OK, but doesn't do anything different at all
```

### Why are functions returning non-`void` assignable to function returning `void`?

Expand Down

0 comments on commit ec64999

Please sign in to comment.