-
-
Notifications
You must be signed in to change notification settings - Fork 203
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
Conversation
packages/queued-request-controller/src/QueuedRequestController.ts
Outdated
Show resolved
Hide resolved
9bd3dae
to
c731d95
Compare
QueuedRequestMessenger
QueuedRequestController
QueuedRequestController
QueuedRequestController
549bad4
to
34ed662
Compare
QueuedRequestController
{Base, QueuedRequest, GasFee}Controller
e1fe71d
to
e0ce7b2
Compare
e0ce7b2
to
ad9dbf8
Compare
{Base, QueuedRequest, GasFee}Controller
{Base, QueuedRequest, GasFee, RateLimit}Controller
ad9dbf8
to
0f75f00
Compare
packages/queued-request-controller/src/QueuedRequestMiddleware.test.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this 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< |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
This comment was marked as resolved.
This comment was marked as resolved.
c56c37e
to
51e0c56
Compare
@mcmire On the subject of
This is a really fun one! So just in empirical terms, changing the Here's why: To achieve this, the return type of this supertype function needs to be
|
This comment was marked as resolved.
This comment was marked as resolved.
76f19c4
to
465a6a8
Compare
Co-authored-by: Mark Stacey <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good!
@Mrtenz Do you have any more concerns about this PR? |
… includes functions with spread argument lists that evlaute as tuple types, not arrays
…-limit-controller`
export type ActionConstraint = { | ||
type: string; | ||
handler: (...args: any) => unknown; | ||
handler: ((...args: never) => unknown) | ((...args: never[]) => unknown); | ||
}; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great sleuthing! 👏🏻
…e and indicate throwaway status
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great!
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 |
base-controller
] Fix any
usage and other type fixesbase-controller
] Fix all any
usage, apply universal supertype for functions
@Gudahtt Changelog updated! |
## 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
…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
… 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)
… 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)
Motivation
The
ControllerMessenger
pattern that is used in all of our controllers currently relies on unsafe typing, which includesany
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
any
usage fromAction
,Event
typing inControllerMessenger
.Action['handler']
with correct universal supertype for functions.SelectorFunction
,EventSubscriptionMap
typing that was causing errors.BaseController
typing by definingNamespacedName
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
deriveStateFromMetadata
Rename all single-letter type param names to be more descriptive.(moved here: [base-controller
] Fix single-letter type param names #2030)Notes on the "universal function supertype"
A.
(...args: never[]) => unknown
Action['handler'] extends ActionConstraint['handler']
, and in generalSubtype extends Supertype
. Therefore, in order to assign any function toAction['handler']
without restrictions, we needActionConstraint['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 benever
(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 messageType 'T[]' is not assignable to type 'never'
.References
any
usage #2026Changelog
@metamask/base-controller
ActionConstraint['handler']
has been fixed to removeany
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.ExtractActionParameters
andExtractActionResponse
have also been fixed to removeany
usage, and to use the new typing ofActionConstraint['handler']
.SelectorFunction
type, replacing itsArgs
generic parameter with an event payload type that is defined with a new required generic parameterEvent
.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
RateLimitedApi['method']
is now constrained by theActionConstraint['handler']
type from the@metamask/base-controller
module.Checklist