Skip to content

Instantly share code, notes, and snippets.

@ackvf
Last active April 29, 2020 11:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ackvf/0f2db59182d4c05b2c31757e55586bcb to your computer and use it in GitHub Desktop.
Save ackvf/0f2db59182d4c05b2c31757e55586bcb to your computer and use it in GitHub Desktop.
Typed compose
/**
* @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);
}
/**
* @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);
}
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