Skip to content
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-controller] Fix all any usage, apply universal supertype for functions #1890

Merged
merged 42 commits into from
Nov 15, 2023

Conversation

MajorLift
Copy link
Contributor

@MajorLift MajorLift commented Oct 23, 2023

Motivation

The ControllerMessenger pattern that is used in all of our controllers currently relies on unsafe typing, which includes any usage in key types. A more rigorous overhaul of these definitions will help us implement features with more type-safe and robust code.

Explanation

Main

  • Removes all any usage from Action, Event typing in ControllerMessenger.
  • Types Action['handler'] with correct universal supertype for functions.
  • Fixes SelectorFunction, EventSubscriptionMap typing that was causing errors.
  • Improves BaseController typing by defining NamespacedName type: the narrowest supertype of all names defined within a given namespace.
  • BaseController now expects a "getState" action type and "stateChange" event type in all controllers.

Auxiliary

Notes on the "universal function supertype"

A. (...args: never[]) => unknown

Action['handler'] extends ActionConstraint['handler'], and in general Subtype extends Supertype. Therefore, in order to assign any function to Action['handler'] without restrictions, we need ActionConstraint['handler'] to be the widest possible function i.e. the supertype of all functions.

To achieve this, the return type of this supertype function needs to be unknown (universal supertype), and the parameters need to be never (universal subtype). This is because in general (x: SuperType) => T is a subtype of (x: SubType) => T. So the params need to be the narrowest type (i.e. never) for the function type to be widened into the universal function supertype.

B. (...args: never) => unknown

In general, array types of arbitrary length are not assignable to tuple types that have fixed types designated to some or all of its index positions (the array is wider than the tuple since it has less constraints).

This means that there's a whole subcategory of functions that (...args: never[]) => unknown doesn't cover: functions that have a finite number of parameters that are strictly ordered and typed, making their spread argument list evaluate as a fixed-length or variadic tuple type, instead of a freely expandable (or contractable) array type.

That is, (...args: never[]) => unknown covers the cases where all of the params can be typed together as a group e.g. (...args: (string | number)[]) => T. But to cover cases like (a: string, b: number, ...rest: unknown[]) => T, we need another top type with an even narrower type for the params: (...args: never) => unknown.

C. ((...args: never) => unknown) | ((...args: never[]) => unknown)

In short, the above represents the top type that includes both of these function categories.

  • | never in general is analogous to + 0, so (...args: (never[] | never)) => unknown doesn't work.
  • (...args: never) => unknown doesn't work with the error message Type 'T[]' is not assignable to type 'never'.

References

Changelog

@metamask/base-controller

  • CHANGED: ActionConstraint['handler'] has been fixed to remove any usage, and is now defined as the universal supertype of all functions, meaning any function can be safely assigned to it, regardless of argument types, number of arguments, or return value type.
  • CHANGED : ExtractActionParameters and ExtractActionResponse have also been fixed to remove any usage, and to use the new typing of ActionConstraint['handler'].
  • CHANGED: BREAKING Alters SelectorFunction type, replacing its Args generic parameter with an event payload type that is defined with a new required generic parameter Event.
  • ADDED: Exports NamespacedName type, which is the narrowest supertype of all names defined within a) a given namespace if a namespace is supplied as a generic parameter, or b) any namespace in general if no generic parameter is provided.

@metamask/rate-limit-controller

  • CHANGED: BREAKING RateLimitedApi['method'] is now constrained by the ActionConstraint['handler'] type from the @metamask/base-controller module.

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've highlighted breaking changes using the "BREAKING" category above as appropriate

Base automatically changed from queue2 to main October 23, 2023 19:59
@MajorLift MajorLift force-pushed the 231023-queued-allowed-actions branch from 9bd3dae to c731d95 Compare October 23, 2023 20:15
@MajorLift MajorLift marked this pull request as ready for review October 23, 2023 21:48
@MajorLift MajorLift requested a review from a team as a code owner October 23, 2023 21:48
@MajorLift MajorLift changed the title Typing fixes and refactors for QueuedRequestMessenger Typing fixesfor QueuedRequestController Oct 23, 2023
@MajorLift MajorLift changed the title Typing fixesfor QueuedRequestController Typing fixes for QueuedRequestController Oct 23, 2023
@MajorLift MajorLift self-assigned this Oct 23, 2023
@MajorLift MajorLift force-pushed the 231023-queued-allowed-actions branch 2 times, most recently from 549bad4 to 34ed662 Compare October 24, 2023 21:17
@MajorLift MajorLift changed the title Typing fixes for QueuedRequestController Typing fixes for {Base, QueuedRequest, GasFee}Controller Oct 24, 2023
@MajorLift MajorLift force-pushed the 231023-queued-allowed-actions branch from e1fe71d to e0ce7b2 Compare October 24, 2023 21:54
@MajorLift MajorLift requested a review from a team as a code owner October 24, 2023 21:54
@MajorLift MajorLift force-pushed the 231023-queued-allowed-actions branch from e0ce7b2 to ad9dbf8 Compare October 24, 2023 22:15
@MajorLift MajorLift changed the title Typing fixes for {Base, QueuedRequest, GasFee}Controller Typing fixes for {Base, QueuedRequest, GasFee, RateLimit}Controller Oct 24, 2023
@MajorLift MajorLift force-pushed the 231023-queued-allowed-actions branch from ad9dbf8 to 0f75f00 Compare October 24, 2023 23:41
@MajorLift MajorLift requested a review from BelfordZ October 25, 2023 17:05
Copy link
Contributor

@mcmire mcmire left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left some comments on the base-controller changes that I was curious about. What do you think about splitting those off into a separate PR? I feel like they could be high impact and so it'd be better to have them in their own PR so they're easier to review now and later.

@@ -65,7 +67,13 @@ export interface StatePropertyMetadata<T extends Json> {
export class BaseController<
N extends string,
S extends Record<string, Json>,
messenger extends RestrictedControllerMessenger<N, any, any, string, string>,
messenger extends RestrictedControllerMessenger<
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is interesting. This would be a breaking change, and so I am wary of changing this. That said, I don't understand why we chose to use any here. I would be curious to hear if Mark can remember any context around this.

Copy link
Contributor Author

@MajorLift MajorLift Oct 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ActionConstraint and EventConstraint in this PR are written to be the universal supertypes of their respective categories, so I don't think we'd be adding a meaningful constraint by moving away from any. Also no tests are being broken by this change, although I guess we'll have to check whether any package outside of the monorepo is using incompatible Action or Event types.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, using any in this context is not harmful in the same way that it is in other contexts. Types used in a generic constraints don't get applied to any specific variables, they're just constraints (i.e. by using any here, absolutely nothing is actually given the type any, even indirectly). For this reason I've sometimes put less effort into avoiding the use of any for generic constraints.

That said, more detailed constraints don't hurt either. And in this case, these constraints are already present in the RestrictedControllerMessenger type so this change seems like it would not affect anything functionally.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generic constraints are also helpful for intellisense hints and self-documenting code. If finding the right constraints isn't a significant blocker for actual feature development, I think it's worth the extra effort, even if it's not strictly necessary in terms of functionality.

packages/base-controller/src/BaseControllerV2.ts Outdated Show resolved Hide resolved
packages/base-controller/src/ControllerMessenger.ts Outdated Show resolved Hide resolved
packages/base-controller/src/ControllerMessenger.ts Outdated Show resolved Hide resolved
packages/base-controller/src/ControllerMessenger.ts Outdated Show resolved Hide resolved
@MajorLift

This comment was marked as resolved.

@MajorLift MajorLift force-pushed the 231023-queued-allowed-actions branch from c56c37e to 51e0c56 Compare October 26, 2023 22:17
@MajorLift
Copy link
Contributor Author

MajorLift commented Oct 27, 2023

@mcmire On the subject of (...args: never[]) => unknown:

This is a really fun one! never[] is actually the logically correct type here, as unintuitive as that seems.

So just in empirical terms, changing the never[] to unknown[] (or even [never]) triggers a whole lot of errors all over the codebase, basically everywhere the Action type is used.

Here's why: Action['handler'] extends ActionConstraint['handler'], and in general Subtype extends Supertype. Therefore, in order to assign any function to Action['handler'] without restrictions, we need ActionConstraint['handler'] to be the widest possible function i.e. the supertype of all functions.

To achieve this, the return type of this supertype function needs to be unknown (universal supertype), and the parameters need to be never (universal subtype). This is because in general (x: SuperType) => T is a subtype of (x: SubType) => T. So the params need to be the narrowest type (i.e. never) for the function type to be widened into the universal function supertype.

@FrederikBolding

This comment was marked as resolved.

@MajorLift MajorLift force-pushed the 231023-queued-allowed-actions branch from 76f19c4 to 465a6a8 Compare November 14, 2023 16:15
Gudahtt
Gudahtt previously approved these changes Nov 14, 2023
Copy link
Member

@Gudahtt Gudahtt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

mcmire
mcmire previously approved these changes Nov 14, 2023
Copy link
Contributor

@mcmire mcmire left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

@mcmire
Copy link
Contributor

mcmire commented Nov 14, 2023

@Mrtenz Do you have any more concerns about this PR?

… includes functions with spread argument lists that evlaute as tuple types, not arrays
@MajorLift MajorLift dismissed stale reviews from mcmire and Gudahtt via 08695f5 November 14, 2023 21:47
Comment on lines 62 to +65
export type ActionConstraint = {
type: string;
handler: (...args: any) => unknown;
handler: ((...args: never) => unknown) | ((...args: never[]) => unknown);
};
Copy link
Contributor Author

@MajorLift MajorLift Nov 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it turns out that (...args: never[]) => unknown probably isn't the universal supertype for functions.

The error messages @Gudahtt linked here #1890 (comment) reflect the fact that in general, array types of arbitrary length are not assignable to tuple types with independent typing for some or all of its index positions (the array is wider than the tuple since it has less constraints e.g. the tuple has a minimum length whereas the array could be empty).

This means that there's a whole subcategory of functions that (...args: never[]) => unknown doesn't cover: functions with at least some parameters that are positionally typed, making their argument list evaluate as a fixed-length or variadic tuple type, instead of a freely expandable (or contractable) array type.

To illustrate, (...args: never[]) => unknown covers the cases where all of the params can be typed together as a group e.g. (...args: (string | number)[]) => T. But to cover cases like (a: string, b: number, ...rest: unknown[]) => T, where positional arguments are typed independently, we need another top type with an even narrower params type: (...args: never) => unknown.

In short, I think the top type for functions that includes both of these categories is: ((...args: never) => unknown) | ((...args: never[]) => unknown).

(| never in general is analogous to + 0, so (...args: (never[] | never)) => unknown doesn't work.)

With this change, CallApi['handler'] is now accepted by RestrictedControllerMessenger as extending ActionConstraint['handler'] without needing the explicit assertion, and running git checkout origin/main ./packages/rate-limit-controller/ on this PR branch no longer results in any type errors.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great sleuthing! 👏🏻

Copy link
Contributor

@mcmire mcmire left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!

Copy link
Member

@Mrtenz Mrtenz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!

@Gudahtt
Copy link
Member

Gudahtt commented Nov 15, 2023

Could you update the PR description to clarify that the action constraint change is no longer breaking? I think with this most recent update that should be the case

@MajorLift MajorLift changed the title [base-controller] Fix any usage and other type fixes [base-controller] Fix all any usage, apply universal supertype for functions Nov 15, 2023
@MajorLift
Copy link
Contributor Author

@Gudahtt Changelog updated!

@MajorLift MajorLift merged commit a356bc3 into main Nov 15, 2023
128 checks passed
@MajorLift MajorLift deleted the 231023-queued-allowed-actions branch November 15, 2023 16:59
MajorLift added a commit that referenced this pull request Nov 15, 2023
## Explanation

Renames all single-letter type parameter names in `base-controller` to
be more descriptive.

## References

- Extracted from #1890

## Changelog

### `@metamask/base-controller`

- **CHANGED**: Renames all single-letter generic parameter names to be
descriptive

## Checklist

- [x] I've updated the test suite for new or updated code as appropriate
- [x] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- [x] I've highlighted breaking changes using the "BREAKING" category
above as appropriate
MajorLift added a commit that referenced this pull request Nov 16, 2023
…t types defined in `base-controller` (#2029)

## Motivation

- The `"getState"` action and `"stateChange"` event are defined in
`base-controller`, and are common to all controllers.
- #1890 defines polymorphic types
`ControllerGetStateAction` and `ControllerStateChangeEvent`.

## Explanation

- This PR refactors all controllers in core so that their respective
`"getState"` action and `"stateChange"` event types are defined using
the `ControllerGetStateAction` and `ControllerStateChangeEvent` types
from `base-controller`.
- The redefined type definitions are exactly equivalent to the previous
typings with no breaking changes introduced.

## Impact

- This accomplishes code de-duplication.
- It makes it possible to propagate any future updates to these types
from a single source.
- All controllers now expect a `"getState"` action and a `"stateChange"`
event, and can accept them without requiring type casting in
`base-controller`.
- `immer` can now be removed as a dependency from 7 packages.

## References

- Extracted from #1890

## Changelog

### `@metamask/base-controller`
- **ADDED**: Exports `ControllerGetStateAction`,
`ControllerStateChangeEvent` types

## Checklist

- [x] I've updated the test suite for new or updated code as appropriate
- [x] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- [x] I've highlighted breaking changes using the "BREAKING" category
above as appropriate
@MajorLift MajorLift mentioned this pull request Nov 22, 2023
legobeat pushed a commit to legobeat/eth-ledger-bridge-keyring that referenced this pull request Dec 3, 2023
… modular types (MetaMask#207)

## Motivation

`IFrameMessageResponse` is currently defined with a generic parameter `TAction`. This parameter doesn't usefully narrow or widen the type. 
- The `action: TAction` property appears to *widen* `IFrameMessageResponse` to accept any action type, but `TAction` is also constrained as a subtype of `IFrameMessageActions`.
- The `TAction` generic param appears to *narrow* `IFrameMessageResponse` to select individual union members, but it interferes with the inference-based type narrowing we can get if `IFrameMessageResponse` is an exhaustively enumerated discriminated union. 

This causes two problems:

1. Type errors on assignment operations that should be valid.
- e.g. [`as` type casting](https://github.com/MetaMask/eth-ledger-bridge-keyring/pull/207/files#diff-c901ebd8641651c9156a86894574a6d9567f2ae79495de0edff2cbef8ae958f0L302-L304) is necessary here because supertype is being assigned to a subtype i.e. `(response: IFrameMessageResponse<TAction>) => void` to `(response: IFrameMessageResponse<IFrameMessageActions>) => void`.
  - In general, `(x: SuperType) => T` is a *subtype* of `(x: SubType) => T`.
- **This error indicates an underlying issue with the typing and shouldn't be suppressed.**
- Removing `TAction` puts `(response: SomeIFrameMessageResponse) => void` on the LHS (assignee, supertype) and `(response: IFrameMessageResponse) => void` on the RHS (assigned, subtype), resolving this logical error.

2. `TAction` is also silencing type errors we should be getting from type narrowing based on `action` value. 
- e.g. Some union members of `IFrameMessageResponse` do not include a `success`, `error`, `payload`, or `payload.error` property, but because of `TAction`, TypeScript doesn't alert us that we should be performing `in` checks on them in addition to null checks. 
- This can cause unexpected failure at runtime, especially from the destructuring assignments that are being used.

Constraining `IFrameMessageResponse['action']` to `IFrameMessageActions` both resolves these issues and guides us towards writing type-safe logic about these actions that conform to their specific type signatures. It appears that this was the original intention of writing `IFrameMessageActions` as a discriminated union instead of a wider type encompassing all possible actions.

## Changes

- **BREAKING** Narrows `IFrameMessageResponse` type definition

## Explanation

- To resolve these issues, This PR makes `IFrameMessageResponse` non-generic and removes `as` casting.
- For improved readability and maintainability, `IFrameMessageResponse` is also refactored into a union of named types.
- A `IFrameMessageResponseBase` type is defined for modularity and better visibility of `IFrameMessageResponse` types with atypical shapes.

## References

- Closes MetaMask#208
- More info on function subtyping: MetaMask/core#1890 (comment)
legobeat pushed a commit to legobeat/eth-ledger-bridge-keyring that referenced this pull request Dec 4, 2023
… modular types (MetaMask#207)

## Motivation

`IFrameMessageResponse` is currently defined with a generic parameter `TAction`. This parameter doesn't usefully narrow or widen the type. 
- The `action: TAction` property appears to *widen* `IFrameMessageResponse` to accept any action type, but `TAction` is also constrained as a subtype of `IFrameMessageActions`.
- The `TAction` generic param appears to *narrow* `IFrameMessageResponse` to select individual union members, but it interferes with the inference-based type narrowing we can get if `IFrameMessageResponse` is an exhaustively enumerated discriminated union. 

This causes two problems:

1. Type errors on assignment operations that should be valid.
- e.g. [`as` type casting](https://github.com/MetaMask/eth-ledger-bridge-keyring/pull/207/files#diff-c901ebd8641651c9156a86894574a6d9567f2ae79495de0edff2cbef8ae958f0L302-L304) is necessary here because supertype is being assigned to a subtype i.e. `(response: IFrameMessageResponse<TAction>) => void` to `(response: IFrameMessageResponse<IFrameMessageActions>) => void`.
  - In general, `(x: SuperType) => T` is a *subtype* of `(x: SubType) => T`.
- **This error indicates an underlying issue with the typing and shouldn't be suppressed.**
- Removing `TAction` puts `(response: SomeIFrameMessageResponse) => void` on the LHS (assignee, supertype) and `(response: IFrameMessageResponse) => void` on the RHS (assigned, subtype), resolving this logical error.

2. `TAction` is also silencing type errors we should be getting from type narrowing based on `action` value. 
- e.g. Some union members of `IFrameMessageResponse` do not include a `success`, `error`, `payload`, or `payload.error` property, but because of `TAction`, TypeScript doesn't alert us that we should be performing `in` checks on them in addition to null checks. 
- This can cause unexpected failure at runtime, especially from the destructuring assignments that are being used.

Constraining `IFrameMessageResponse['action']` to `IFrameMessageActions` both resolves these issues and guides us towards writing type-safe logic about these actions that conform to their specific type signatures. It appears that this was the original intention of writing `IFrameMessageActions` as a discriminated union instead of a wider type encompassing all possible actions.

## Changes

- **BREAKING** Narrows `IFrameMessageResponse` type definition

## Explanation

- To resolve these issues, This PR makes `IFrameMessageResponse` non-generic and removes `as` casting.
- For improved readability and maintainability, `IFrameMessageResponse` is also refactored into a union of named types.
- A `IFrameMessageResponseBase` type is defined for modularity and better visibility of `IFrameMessageResponse` types with atypical shapes.

## References

- Closes MetaMask#208
- More info on function subtyping: MetaMask/core#1890 (comment)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[base-controller] fix: any usage
6 participants