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:
- The
<Wrapped>
is statically listing thename
, but removing this property does not show the error message in vs code (nay :( ). - The
<MyHocComponent>
properly listslastName
and noname
. RemovinglastName
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" />
)
}
}