[base-controller] Fix all any usage, apply universal supertype for functions#1890
[base-controller] Fix all any usage, apply universal supertype for functions#1890
base-controller] Fix all any usage, apply universal supertype for functions#1890Conversation
packages/queued-request-controller/src/QueuedRequestController.ts
Outdated
Show resolved
Hide resolved
9bd3dae to
c731d95
Compare
QueuedRequestMessengerQueuedRequestController
QueuedRequestControllerQueuedRequestController
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
mcmire
left a comment
There was a problem hiding this comment.
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.
| 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.
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.
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.
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.
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]>
|
@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.
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.
…e and indicate throwaway status
|
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
ControllerMessengerpattern that is used in all of our controllers currently relies on unsafe typing, which includesanyusage in key types. A more rigorous overhaul of these definitions will help us implement features with more type-safe and robust code.Explanation
Main
anyusage fromAction,Eventtyping inControllerMessenger.Action['handler']with correct universal supertype for functions.SelectorFunction,EventSubscriptionMaptyping that was causing errors.BaseControllertyping by definingNamespacedNametype: the narrowest supertype of all names defined within a given namespace.BaseControllernow expects a "getState" action type and "stateChange" event type in all controllers.Auxiliary
deriveStateFromMetadataRename 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[]) => unknownAction['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) => Tis 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) => unknownIn 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[]) => unknowndoesn'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[]) => unknowncovers 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.
| neverin general is analogous to+ 0, so(...args: (never[] | never)) => unknowndoesn't work.(...args: never) => unknowndoesn't work with the error messageType 'T[]' is not assignable to type 'never'.References
anyusage #2026Changelog
@metamask/base-controllerActionConstraint['handler']has been fixed to removeanyusage, 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.ExtractActionParametersandExtractActionResponsehave also been fixed to removeanyusage, and to use the new typing ofActionConstraint['handler'].SelectorFunctiontype, replacing itsArgsgeneric parameter with an event payload type that is defined with a new required generic parameterEvent.NamespacedNametype, 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-controllerRateLimitedApi['method']is now constrained by theActionConstraint['handler']type from the@metamask/base-controllermodule.Checklist