Backpropagera typinferens vid currying i TypeScript


Jag hoppas att titeln är begriplig. Det jag vill göra är att ha en fabrikationsfunktion som tar en funktion som tar ett argument som kommer att tillhandahållas vid ett senare anrop av den returnerade funktionen. I huvudsak:

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
}

Självklart är A inte tillgänglig för den första funktionsdefinitionen och den måste finnas i den andra funktionsdefinitionen för att kunna härledas korrekt (se min tidigare fråga Hur man skriver omit-funktion med korrekta typer i TypeScript).

Jag antar att det åtminstone kan finnas ett sätt att begränsa A till både A extends Record<B, any> eftersom det behövs så att det första argumentet faktiskt är en nyckel från objektet som tillhandahålls senare, samtidigt som det måste vara samma som fn-egenskaperna.

Exemplet är konstruerat men i huvudsak bör något liknande behövas för redux connect-stil HOC. Problemet är att jag inte förstår redux typdefinitioner tillräckligt för att veta hur man tar dem och modifierar för mitt användningsfall.

REDIGERA: Exempel på den HOC jag vill skapa:

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} />)

Svar #1

Hittills har jag lyckats delvis göra det jag vill och jag tror att det förmodligen är det enda som är möjligt här, som nämnts av @jcalz i en kommentar.

I det allmänna exemplet:

// 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

Och i HOC-exemplet:

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} />)

Så detta verkar vara tillräckligt typ-säkert och korrekt för mitt användningsfall, min enda invändning är att jag skulle vilja ha typfelet i actionFunction istället för att ha det i komponentanvändningen senare. Troligtvis inte möjligt, men jag kommer att lämna detta öppet ett tag för att se om det finns någon som vet ett sätt.