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

Can we get some clear guidance on HOCs? #29175

Closed
atrauzzi opened this issue Dec 27, 2018 · 5 comments
Closed

Can we get some clear guidance on HOCs? #29175

atrauzzi opened this issue Dec 27, 2018 · 5 comments
Labels
Domain: JSX/TSX Relates to the JSX parser and emitter Question An issue which isn't directly actionable in code

Comments

@atrauzzi
Copy link

atrauzzi commented Dec 27, 2018

Update: I'm quite certain I'm not alone when I say I'm at my limit with this.

I'm aware that there are a significant number of tickets where people are attempting to come up with correct typings for their React higher order components. That problem appears to be solved by the community with the types I'm including in the first two lines of my snippet - however my problem differs slightly...

I'm defining an HOC that excludes based on the keys provided to the second parameter of my factory. You'll see this in my example as injectionMap. Its usage looks like this:

reactInjectDictionary(MyComponent, {
    "serviceOne": mySymbols.ServiceOne,
    "serviceTwo": mySymbols.ServiceTwo,
});

The idea here is that any key in the dictionary should:

  • Be a valid key in the props of MyComponent
  • Be removed from the props of the HOC

From what I can see, I might actually have the second item working, but I have some issues which are still causing errors that don't make sense:

  • I get no type checking for the candidate keys when authoring my map, even though I really feel like I've done my part to outline what they could be.
  • In the code below, I get this error on the line after ITEM 1

    Type 'InjectedKeys' does not satisfy the constraint 'keyof Props'.
    Type 'keyof InjectionMap' is not assignable to type 'keyof Props'.
    Type 'string | number | symbol' is not assignable to type 'keyof Props'.
    Type 'string' is not assignable to type 'keyof Props'.ts(2344)

  • In the code below, I get this error on the line after ITEM 2

    Argument of type 'InjectionMap' is not assignable to parameter of type 'ArrayLike<{}> |
    Dictionary<{}> | NumericDictionary<{}> | null | undefined'.
    Type 'InjectionMap' is not assignable to type 'Dictionary<{}>'.ts(2345)

type GetProps<C> = C extends React.ComponentType<infer P> ? P : never;
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

export function reactInjectDictionary
<
    ComponentType extends React.ComponentType<any>,
    Props = GetProps<ComponentType>,
    PropsKey extends keyof Props = keyof Props,
    InjectionMap = {[key in PropsKey]: symbol},
    InjectedKeys extends keyof InjectionMap = keyof InjectionMap
>
(
    ComponentType: ComponentType, 
    injectionMap: InjectionMap
) {
    // ITEM 1
    return function ComponentWithInjection(props: Omit<Props, InjectedKeys>) {

        return <BundleConsumer>
        {
            (bundle) => {

                if (!bundle) {

                    throw new Error("This portion of the React component graph is built using protoculture-react-native.  Please be sure to wrap it with a `BundleProvider`.");
                }

                // ITEM 2
                const injections = _.mapValues(injectionMap, (symbol) =>
                    bundle.container.get(symbol));

                return <ComponentType
                    {...props}
                    {...injections}
                />
            }
        }
        </BundleConsumer>;
    };
}

Some of this feels like it's starting to work, but there are just the few snags I've outlined above which make no sense to me based on what I'm being told. My rationale is this:

  • Item 1 - It has to be a key of Props because the type of InjectedKeys is derived from Props via InjectionMap, which in turn is derived from PropsKey.
  • Item 2 - How can this not be a dictionary? The definition of the type InjectionMap is a statically defined dictionary: {[key in PropsKey]: symbol}

It could be that I'm running afoul of something the type system knows that I don't, but the way it's being conveyed contradicts some of how it's coming together as well. The overall goal here is that any key provided as that second parameter to reactInjectDictionary should be subtracted from the resultant Props signature of the generated HOC.

cc.
@atrauzzi
Copy link
Author

export interface FormContext<DataType> {
    form: FormContextUtilities<DataType>;
    immutable: boolean;
    index: number;
    data: DataType;
    validationErrors: ValidationErrors<DataType>;
    schema?: Yup.Schema<DataItemType<DataType>>;
}

export function withForm
<
    DataType,
    ContextType extends UsesFormContext<DataType>,
    ComponentType extends React.ComponentType<ContextType>,
    FormComponentProps extends GetProps<ComponentType>,
    WrappedComponentProps extends Omit<FormComponentProps, keyof ContextType>
>
// ITEM 1
(FormComponent: React.ComponentType<FormComponentProps>): React.ComponentType<WrappedComponentProps> {

    return (props: WrappedComponentProps) => <FormConsumer>
    {
        // ITEM 2
        (context: UsesFormContext<DataType>) => <FormComponent
            {...context as any}
            {...props as any}
        />
    }
    </FormConsumer>
}
Item 1

One way I can make this issue go away and - seemingly - get the desired outcome is to do this for my HOC factory signature:

(FormComponent: React.ComponentType<FormComponentProps | WrappedComponentProps>)

I'm not going to lie, the errors I'm getting back have me in "guess and check" mode right now. Not entirely sure what a correct solution would be in this situation.

Item 2

If I remove the as any from the two prop spreads being passed to FormComponent, I get an error. This error is similar to the ones in my original ticket description where I'm being told that the sum of the type derived from my Omit combined with the omitted list is not equal to the original input.

Type '{ form: FormContextUtilities<DataType, DataItemType>; immutable: boolean; index: number; data: DataType; validationErrors: ValidationErrors<DataType, DataItemType>; schema?: Schema<...>; } & WrappedComponentProps' is not assignable to type 'IntrinsicAttributes & FormComponentProps & { children?: ReactNode; }'.
Type '{ form: FormContextUtilities<DataType, DataItemType>; immutable: boolean; index: number; data: DataType; validationErrors: ValidationErrors<DataType, DataItemType>; schema?: Schema<...>; } & WrappedComponentProps' is not assignable to type 'FormComponentProps'.

@atrauzzi atrauzzi changed the title Types for a 🚨 different 🚨 kind of React HOC (dynamic keys for removal) Can we get some clear guidance on HOCs? Dec 30, 2018
@weswigham weswigham added Question An issue which isn't directly actionable in code Domain: JSX/TSX Relates to the JSX parser and emitter labels Jan 3, 2019
@ferdaber
Copy link

@atrauzzi this would be the way to do it, unfortunately there's a current bug that prevents the spread props from being recombined properly:

import React from 'react'

declare const _: {
  mapValues<T extends Record<string, any>, U>(obj: T, mapFn: (val: T[keyof T]) => U): Record<keyof T, U>
}

export function reactInjectDictionary<
  Props,
  InjectedKeys extends keyof Props
>
(
    ComponentType: React.JSXElementConstructor<Props>, 
    injectionMap: Record<InjectedKeys, symbol>
) {
    // ITEM 1
    return function ComponentWithInjection(props: Omit<Props, InjectedKeys>) {
        return <BundleConsumer>
        {
            (bundle) => {
                if (!bundle) {
                    throw new Error("This portion of the React component graph is built using protoculture-react-native.  Please be sure to wrap it with a `BundleProvider`.");
                }

                // ITEM 2
                const injections: Pick<Props, InjectedKeys> = _.mapValues(injectionMap, (symbol) =>
                    bundle.container.get(symbol));

                return <ComponentType
                    {...props as Props}
                    {...injections}
                />
            }
        }
        </BundleConsumer>;
    };
}

@ferdaber
Copy link

the {...props as Props} is the workaround for this current bug: #28938

@ferdaber
Copy link

ferdaber commented Jan 17, 2019

Note the annotation for injections as well, we need to give TypeScript some assistance in making the type stronger, it's not just a record of injected keys that have union values, it's precisely the map from injectedMap to their original types in Props

@typescript-bot
Copy link
Collaborator

This issue has been marked as 'Question' and has seen no recent activity. It has been automatically closed for house-keeping purposes. If you're still waiting on a response, questions are usually better suited to stackoverflow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Domain: JSX/TSX Relates to the JSX parser and emitter Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

5 participants
@atrauzzi @weswigham @ferdaber @typescript-bot and others