
Question:
I hope the title makes sense. What I want to do is to have a factory function that takes a function taking an argument which will be supplied in later call of the returned function. In essence:
const f = <B extends keyof any>(arg: B, fn: (props: A) => void) => <A extends Record<B, any>>(obj: A): Omit<A, B> => {
fn(obj)
delete obj[arg]
return obj
}
Obviously A
is not available to the first function definition and it has to be in the second function definition to be inferred properly (see my previous question <a href="https://stackoverflow.com/questions/53466131/how-to-write-omit-function-with-proper-types-in-typescript" rel="nofollow">How to write omit function with proper types in typescript</a>).
I assume there could be at least a way to constrain A
to both A extends Record<B, any>
as that is needed so that the first arg is actually a key from the object supplied later, while at the same time it has to be the same as the fn
props.
The example is contrived but in essence something similar should be needed for redux connect style HOC. Issue is I do not understand redux type definitions enough to know how to take them and modify for my usecase.
EDIT: Example of the HOC I want to create:
export const withAction = <A extends keyof any, B extends object>(
actionName: A,
// The B here should actually be OuterProps
actionFunc: (props: B, ...args: any[]) => Promise<any>,
) => <InnerProps extends object, OuterProps extends Omit<InnerProps, A>>(
WrappedComponent: React.ComponentType<InnerProps>,
): React.ComponentType<OuterProps> => {
return (props: OuterProps) => {
// This is a react hook, but basically it just wraps the function with some
// state so the action here is an object with loading, error, response and run
// attributes. We just need to wrap it like this to be able to reuse hook
// as a HOC for class based components.
const action = useAction((...args) => {
// At this moment the props here does not match the function arguments and
// it triggers a TS error
return actionFunc(props, ...args)
})
// The action is injected here as the actionName
return <WrappedComponent {...props} {...{ [actionName]: action }} />
}
}
// Usage:
class Component extends React.Component<{ id: number, loadData: any }> {}
// Here I would like to check that 'loadData' is actually something that the
// component want to consume and that 'id' is a also a part of the props while
// 'somethingElse' should trigger an error which it does not at the moment.
const ComponentWithAction = withAction('loadData', ({ id, somethingElse }) =>
API.loadData(id),
)(Component)
// Here the ComponentWithAction should be React.ComponentType<{id: number}>
render(<ComponentWithAction id={1} />)
Answer1:So far I managed to partially do what I want and I think it is probably only thing that is possible here as mentioned by @jcalz in a comment.
In the general example:
// Adding '& C' to second generic params
const f = <B extends keyof any, C extends object>(arg: B, fn: (props: C) => void) => <A extends Record<B, any> & C>(obj: A): Omit<A, B> => {
fn(obj)
delete obj[arg]
return obj
}
const a = f('test', (props: { another: number }) => {})
// TS error, another: number is missing which is good
const b = a({ test: 1 })
const d = a({ test: 1, another: 1 })
// test does not exist which is good
const c = d.test
And in the HOC example:
export const withAction = <A extends keyof any, B extends object>(
actionName: A,
actionFunc: (props: B, ...args: any[]) => Promise<any>,
) => <
InnerProps extends object,
// The only change is here that OuterProps has '& B'
OuterProps extends Omit<InnerProps, A> & B
>(
WrappedComponent: React.ComponentType<InnerProps>,
): React.ComponentType<OuterProps> => {
return (props: OuterProps) => {
const action = useAction((...args) => {
return actionFunc(props, ...args)
})
return <WrappedComponent {...props} {...{ [actionName]: action }} />
}
}
// Usage:
class Component extends React.Component<{ id: number; loadData: any }> {}
const ComponentWithAction = withAction(
'loadData',
// somethingElse here does not trigger the error which would be nice but
// probably not possible
(props: { id: number; somethingElse: number }) => API.loadData(props.id),
)(Component)
// Here the ComponentWithAction is React.ComponentType<{id: number, somethingElse: number }>
// and so TS correctly errors that somethingElse is missing
render(<ComponentWithAction id={1} />)
// This is correct but strangely some Webstorm inspection thinks loadData is
// required and missing here. So far first time I see Webstorm trip with TS.
render(<ComponentWithAction id={1} somethingElse={3} />)
So this seem to be type safe enough and correct for my usecase, my only nitpick is that I would like to have the type error in the actionFunction instead of having it in the component usage later on. Probably not possible but I will leave this open for some time to see if there is nobody who would know a way.