Last active
April 29, 2020 11:42
-
-
Save ackvf/0f2db59182d4c05b2c31757e55586bcb to your computer and use it in GitHub Desktop.
Typed compose
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @name compose | |
* @summary Composes Higher-Order-Functions from right to left so that they are executed from left to right. | |
* note: To compose Higher-Order-Components, use compose.ts | |
* | |
* | |
* @description | |
* Two overloads are available: | |
* A) Matches the composed signature of whole compose to the wrapped function. | |
* B) As an escape hatch, it is possible to explicitly define the resulting OuterSignature with a generic, ignoring the HOFs types. | |
*/ | |
export function compose( | |
...hofs: HOF[] | |
): <F extends AnyFunction>(wrapped: F) => F; | |
export function compose<OuterSignature>( | |
...hofs: AnyFunction[] | |
): (wrapped: AnyFunction) => OuterSignature; | |
export function compose(...hofs) { | |
return wrapped => hofs.reduceRight((p, c) => c(p), wrapped); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @name compose | |
* @summary Composes HOCs to pass props from left to right. It tries to infer the types automatically from usage. | |
* note: To compose Higher-Order-Functions, use compose from compose-functions.ts | |
* | |
* | |
* @example // A - with automatic type inferrence -- if type inferrence doesn't produce any props, some HOC is incompatible, try casting it (read NOTE below) | |
* const ProductDetail = compose( | |
* withRouter as HOC<RouteComponentProps<IProductDetailRouteParams>>, // cast incompatible HOC to allow type inferrence | |
* withFeatures, | |
* withProductView, | |
* )(ProductDetailContainer); | |
* | |
* | |
* @example // B - with own provided OuterProps Interface - suppresses type iferrence | |
* const Enhanced = compose<{productId: number}>( | |
* withRouter, | |
* withFeatures, | |
* withProductView, | |
* )(ProductDetailContainer); | |
* | |
* | |
* @description | |
* This composer tries to infer resulting Outer PropTypes by looking at | |
* A) WrappedComponent's PropTypes | |
* B) all HOC's OuterInterface Generic | |
* C) all HOC's InnerInterface Generic | |
* - if no Generic is provided, it tries to infer the types from the signatures | |
* | |
* The result is then optimistically calculated as (A + B) - C | |
* | |
* As an escape hatch, it is possible to explicitly define the resulting OuterInterface with a generic, ignoring the HOCs types. | |
* | |
* | |
* !! NOTE !! | |
* Currently there is a limitation that prevents correct compose-HOC type inferrence when the Outer and Inner | |
* interfaces of a HOC overlap (meaning OuterProps are also on the Wrapped component) as they cancel each other out B - C. | |
* The problem occurs with `withRouter`, any `graphql` hoc or any which have signature similar to this: | |
* ```ts | |
* function withQuery(query,options): (WrappedComponent: React.ComponentType<OuterProps & GqlResponse>) => React.ComponentClass<OuterProps, any> | |
* ``` | |
* To work around this issue, you can cast the hoc at call site as in example above, or override declaration with type HOC: | |
* ```ts | |
* export const withProductView: HOC<GqlChildProps, OuterProps> = withQuery(query, options) as any; | |
* ``` | |
* @see HOC definition for another example. | |
*/ | |
export function compose<HOCs extends Array<HOC<any, any>>>( | |
...hocs: HOCs | |
): <InnerComponent extends React.ComponentType>( | |
WrappedComponent: InnerComponent | |
) => React.ComponentType< | |
Omit< | |
ExtractComponentGenerics<InnerComponent> & ExtractHOCOuterInterfaces<HOCs>, | |
keyof ExtractHOCInnerInterfaces<HOCs> | |
> | |
>; | |
export function compose<OuterInterface extends {}>( | |
...hocs: Array<HOC<any, any>> | |
): ( | |
WrappedComponent: React.ComponentType | |
) => React.ComponentType<OuterInterface>; | |
export function compose(...hocs) { | |
return wrapped => hocs.reduceRight((p, c) => c(p), wrapped); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
interface AnyObject { | |
[key: string]: any; | |
} | |
type AnyFunction = (...args: any[]) => any; | |
type AnyAsyncFunction = (...args: any[]) => Promise<any>; | |
type RecursivePartial<T> = { [P in keyof T]?: RecursivePartial<T[P]> }; | |
type UnionToIntersection<U> = (U extends any | |
? (k: U) => void | |
: never) extends (k: infer I) => void | |
? I | |
: never; | |
/** | |
* @name HOF | |
* @summary Type definition for any Higher-Order-Function to ease typing. | |
* | |
* @description | |
* Higher order function accepts a function F and returns another function, | |
* that has the same Parameters and ReturnType as F. | |
* | |
* It could as well be written in this form: | |
* `<F extends AnyFunction>(wrapped: F) => (args: Parameters<F>): ReturnType<F> => wrapped(args)` | |
*/ | |
type HOF<F = AnyFunction> = (wrapped: F) => F; | |
/** | |
* @name HOC | |
* @summary Type definition for any Higher-Order-Component to provide type safety and easier composability | |
* | |
* @example | |
* declare const MyComponent: React.FC<{ownProp, inner}> | |
* const withHoc: HOC<{inner}, {outer}> = (WrappedComponent: React.ComponentType<{inner}>) => ({outer, ...rest}) => <WrappedComponent inner={outer} {...rest}/> | |
* const Enhanced = withHoc(MyComponent) | |
* const App = <Enhanced outer ownProp/> // ownProp is inferred automatically | |
* | |
* @description | |
* This HOC tries to infer resulting Outer PropTypes by looking at | |
* A) WrappedComponent's PropTypes | |
* B) HOC's own OuterInterface Generic | |
* C) HOC's own InnerInterface Generic | |
* | |
* The result is then optimistically calculated as B + (A - C) | |
* | |
* | |
* Define only the minimum interfaces your HOC needs, do not couple it with Consumer's props or other HOCs interfaces. | |
* Also don't forget to pass through {...rest} props to the WrappedComponent. | |
*/ | |
type HOC<InnerInterface = {}, OuterInterface = {}> = < | |
InnerComponent extends React.ComponentType<InnerInterface> | |
>( | |
WrappedComponent: InnerComponent | |
) => React.ComponentType< | |
OuterInterface & | |
Omit<ExtractComponentGenerics<InnerComponent>, keyof InnerInterface> | |
>; | |
type ExtractComponentGenerics< | |
T extends React.ComponentType | AnyFunction | |
> = T extends React.ComponentType<infer G> ? G : never; | |
type ExtractHOCInnerInterface<T extends HOC> = Parameters<T>[0] extends | |
| React.ComponentType | |
| AnyFunction | |
? ExtractComponentGenerics<Parameters<T>[0]> | |
: never; | |
type ExtractHOCOuterInterface<T extends HOC> = ReturnType<T> extends | |
| React.ComponentType | |
| AnyFunction | |
? ExtractComponentGenerics<ReturnType<T>> | |
: never; | |
type ExtractHOCInnerInterfaces<T extends HOC[]> = T extends Array<infer U> // @ts-ignore // Type 'U' does not satisfy the constraint 'AnyFunction'.ts(2344) - https://github.com/microsoft/TypeScript/issues/34604 | |
? UnionToIntersection<ExtractHOCInnerInterface<U>> | |
: never; | |
type ExtractHOCOuterInterfaces<T extends HOC[]> = T extends Array<infer U> // @ts-ignore // Type 'U' does not satisfy the constraint 'AnyFunction'.ts(2344) - https://github.com/microsoft/TypeScript/issues/34604 | |
? UnionToIntersection<ExtractHOCOuterInterface<U>> | |
: never; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment