Home > Blockchain >  Fully static HOC in react and typescript
Fully static HOC in react and typescript

Time:09-26

I have this code:


type AAdds = {
  name: string
}
type WithAAdds<T> = T & Partial<AAdds>

const withMyHoc = function<P>(Wrapped: FC<WithAAdds<P>>) {
  return (props: P) => {
    return (
      <Wrapped name="john" {...props} />
    )
  }
}

const MyComponent = ({name, lastName}: {name:string, lastName: string}) => {
  return (
    <div>
      {name} : {lastName}
    </div>
  )
}

const MyHocComponent = withMyHoc(MyComponent)

const App = () => {
  // Property 'name' is missing in type '{ lastName: string; }' but required in type '{ name: string; lastName: string; }'.ts(2741)
  return <MyHocComponent lastName="Doe" />
}

I want the <MyHocComponent /> to be statically typed and without listing name in vs code. Is this possible?

I tried adding this additional type:

type WithoutAAdds<T> = Pick<T, Exclude<keyof AAdds, 'name'>>

And setting the HOC's return type to FC<WithoutAAdds<P>>, but that fails too with

Type '(props: P) => JSX.Element' is not assignable to type 'FC<WithoutAAdds<P>>'.
  Types of parameters 'props' and 'props' are incompatible.
    Type 'PropsWithChildren<WithoutAAdds<P>>' is not assignable to type 'P'.
      'P' could be instantiated with an arbitrary type which could be unrelated to 'PropsWithChildren<WithoutAAdds<P>>'.ts(2322)

This comes from the return signature (props: P).

CodePudding user response:

So "fully static" does not seem to be possible. Perhaps someone can improve this, but it does address the original problem:

  1. The <Wrapped> is statically listing the name, but removing this property does not show the error message in vs code (nay :( ).
  2. The <MyHocComponent> properly lists lastName and no name. Removing lastName does give an error in vs code (yay!).

The only roadblock to a "fully static" HOC is the any for the inner props. I could not find a workaround to make that statically typed. The first observation above seems to stem from this roadblock.

PS: Thanks to @Zbigniew Zagórski and @Alex Chashin for Omit type.

type AAdds = {
  name: string
}
type WithAAdds<T> = T & AAdds
type WithoutAAdds<T> = Omit<T, keyof AAdds>

const withMyHoc = function <P>(Wrapped: FC<WithAAdds<P>>): FC<WithoutAAdds<P>> {
  return (props: any) => {
    return (
      <Wrapped name="john" {...props} />
    )
  }
}

const MyComponent = ({ name, lastName }: { name: string, lastName: string }) => {
  return (
    <div>
      {name} : {lastName}
    </div>
  )
}

const MyHocComponent = withMyHoc(MyComponent)

const App = () => {
  return <MyHocComponent lastName="Doe" />
}

CodePudding user response:

The first thing I did was copy and paste your code into the TypeScript Playground. By hovering over withMyHoc(MyComponent) I can see the inferred type for P is {name: string; lastName: string}. But we want P to be {lastName: string} only, and for name to be added by WithAAdds.

So then I tried to see if it would work I explicitly set the value for P rather than letting it be inferred. Like this:

const MyHocComponent = withMyHoc<{lastName: string}>(MyComponent)

This revealed your first error. T & Partial<AAdds> says the name property is optional on the outer component because of the Partial. This won't work because MyComponent requires name.

You need to make the added properties be requirements.

type WithAAdds<T> = T & AAdds

This is progress, because now the above code where we manually specified withMyHoc<{lastName: string}> will work, and name won't be required in MyHocComponent.

You can also do this:

const MyHocComponent = withMyHoc<{lastName: string}>(({name, lastName}) => {
  return (
    <div>
      {name} : {lastName}
    </div>
  )
})

But we still aren't getting the proper type inference.


The Exclude/Omit approach (Omit uses Exclude under the hood) allows for automatic type inference of P, but it introduces a possibility for an edge case problem where P could be {name: "Bob", lastName: string}. That's why you get the dreaded "could be instantiated with an arbitrary type" error.

If you want to ignore that edge case, you can just assert correctness.

const withMyHoc = function<P extends AAdds>(Wrapped: FC<P>) {
  return (props: Omit<P, keyof AAdds>) => {
    return (
      <Wrapped {...{...props, name: "john"} as P} />
    )
  }
}

This version is more readable, but less precise, since we are asserting that props is P when we know that it isn't because it's missing the name.

const withMyHoc = function<P extends AAdds>(Wrapped: FC<P>) {
  return (props: Omit<P, keyof AAdds>) => {
    return (
      <Wrapped {...props as P} name="john" />
    )
  }
}
  • Related