-
-
Notifications
You must be signed in to change notification settings - Fork 42
TypeScript guidelines: Document any requirement for callback parameters subject to bidirectional assignment
#170
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
77548ad
7849776
b663cbc
a3b2ec1
5f5cc8f
7c49096
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -881,6 +881,58 @@ export class ComposableController< | |
| const controllerMessenger = ControllerMessenger<any, any>; | ||
| ``` | ||
|
|
||
| #### `any` is required for callback parameter types constrained by bidirectional assignment | ||
|
|
||
| This shape arises whenever a callback value is constrained on both sides of an assignment by _different_ external function types. Real-world instances: | ||
|
|
||
| - A `coerces` map sitting between a library signature and a caller's own config — see [`metamask-extension#41104 (r3045807022)`](https://github.com/MetaMask/metamask-extension/pull/41104#discussion_r3045807022). | ||
| - A messenger's `registerActionHandler` slot typed `(...args: any[]) => any`: it receives strongly-typed handler callbacks inward at registration _and_ is invoked with strongly-typed argument tuples outward at dispatch. `unknown[]` fails registration; `never[]` fails dispatch. | ||
|
|
||
| Under `--strictFunctionTypes`, function parameters are checked _contravariantly_: `(arg: A) => R` is assignable to `(arg: B) => R` only when `B extends A`. Parameter types flow in the _reverse_ direction of the assignment. | ||
|
|
||
| A callback (or a `Record` of callbacks) hits an impossible constraint when its parameter position is constrained by _two different_ external function types at once: | ||
|
|
||
| 1. **Outward** — the callback flows into a slot of another function type. Contravariance requires its parameter to be a _supertype_ of that slot's parameter. `unknown` ✓, `never` ✗. | ||
| 2. **Inward** — another function value is assigned into the callback's slot. Contravariance requires its parameter to be a _subtype_ of the incoming value's parameter. `never` ✓, `unknown` ✗. | ||
|
|
||
| When both directions apply to the same parameter position, no concrete type satisfies both unless one external param already extends the other — which external APIs rarely guarantee. `any` is the only inhabitant of both the top and bottom of the assignability lattice, and is the only escape. Return types remain covariant and can stay `unknown`. | ||
|
|
||
| | Parameter type | Outward (supertype of outer param) | Inward (subtype of outer param) | | ||
| | -------------- | ---------------------------------- | ------------------------------- | | ||
| | `unknown` | ✓ | ✗ | | ||
| | `never` | ✗ | ✓ | | ||
| | `any` | ✓ | ✓ | | ||
|
|
||
| **Example <a id="example-f2a3b7d1-9e4c-4f8a-b6c2-1d8e5a3c9f7b"></a> ([🔗 permalink](#example-f2a3b7d1-9e4c-4f8a-b6c2-1d8e5a3c9f7b)):** | ||
|
|
||
| A callback's parameter constrained from both sides — outward by a wider external type, inward by a narrower one: | ||
|
|
||
| ```typescript | ||
| type Wide = (x: string) => void; | ||
| type Narrow = (x: 'a') => void; | ||
|
|
||
| declare function takesWide(f: Wide): void; | ||
| declare const givesNarrow: Narrow; | ||
|
|
||
| // 🚫 `unknown` — satisfies outward, fails inward | ||
| let f1: (x: unknown) => void; | ||
| takesWide(f1); // ✓ | ||
| f1 = givesNarrow; // ✗ 'unknown' not assignable to '"a"' | ||
|
|
||
| // 🚫 `never` — satisfies inward, fails outward | ||
| let f2: (x: never) => void; | ||
| f2 = givesNarrow; // ✓ | ||
| takesWide(f2); // ✗ 'string' not assignable to 'never' | ||
|
|
||
| // ✅ `any` — satisfies both directions | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| let f3: (x: any) => void; | ||
| takesWide(f3); // ✓ | ||
| f3 = givesNarrow; // ✓ | ||
| ``` | ||
|
|
||
| > **Note:** The `eslint-disable` is intentional. `any` here is _not_ infectious: it is scoped to a single callback's parameter position and does not propagate to callers. The enclosing external APIs re-impose their own parameter types at each use site, so type safety is preserved where values actually flow. Prefer `unknown` or `never` when only one direction applies; reach for `any` only when both apply to the same parameter position. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure about how this guideline is framed. This note says that this is safe because "The enclosing external APIs re-impose their own parameter types at each use site", and indeed that is why this pattern is safe in the rare case where we've used it. But this statement is not true of all situations where you might be tempted to use
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not totally sure what to suggest though. I am having trouble describing the safe scenarios here. It's more than just a matter of encapsulation. We could encapsulate an instance of an invariant type causing a type error, and it still be a legitimate error. We need to be confident the error would never occur (e.g. we'll never attempt to pass in a parameter that doesn't match the callback parameter type, and we'll never return a value that doesn't match the callback return type)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes I'll clarify that sometimes we can't "re-impose types" and If it's possible to re-type either constraint to resolve an incompatible configuration without affecting downstream callers or introducing semantic inaccuracies, the type error does signal broken design and we shouldn't use Will push a new draft based on this. |
||
|
|
||
| ### Type-Only Dependencies | ||
|
|
||
| If package `a` imports only types from `b`, should `b` be a dev or production dependency of `a`? | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.