Reputation: 77
I'm trying to figure out how to write the correct type for the Data entry so that it accepts any component with props
Example:
const A = (props: { a: number }) => <>{props.a}</>
const B = (props: { b: string }) => <>{props.b}</>
type Data = {
[id: string]: <P>(props: P) => JSX.Element | null
}
const data: Data = {
aa: (props: Parameters<typeof A>[0]) => A(props),
bb: (props: Parameters<typeof B>[0]) => B(props)
}
const Usage = () => (
<>
A: {data.aa({ a: 12 })}
B: {data.bb({ b: 'hi' })}
</>
)
Even though I've specified prop types for each component, I'm still getting this error:
TS2322: Type '(props: Parameters<typeof A>[0]) => JSX.Element' is not assignable to type '<P>(props: P) => Element | null'.
Types of parameters 'props' and 'props' are incompatible.
Type 'P' is not assignable to type '{ a: number; }'
What should I write in the Data
type so it accepts any component?
Upvotes: 1
Views: 2276
Reputation: 94
I like what Chris Hamilton mentioned with using functional components class from react. I read this question more about how to deal with typescript generic types with constraints. The best I could see is to create a union type of AProps and BProps for defining the type constraint in the Data type definition and using typeguards to enforce what is used in each Data function. It however, would get unwieldy if you want this to work for any type of component at all because you have to define the union type for all possible components.
type AProps = { a: number };
type BProps = { b: string };
const A = (props: AProps) => <>{props.a}</>
const B = (props: BProps) => <>{props.b}</>
type GenericParams = AProps | BProps;
type Data = {
[id: string]: <P extends GenericParams>(props: P) => JSX.Element | null
}
const data: Data = {
aa: (props: GenericParams) => isAProps(props) ? A(props) : null,
bb: (props: GenericParams) => isBProps(props) ? B(props) : null
}
const Usage = () => (
<>
A: {data.aa({ a: 12 })}
B: {data.bb({ b: 'hi' })}
</>
)
function isAProps(props: GenericParams): props is AProps
{
if ("a" in props && typeof props.a === "number")
return true;
return false;
}
function isBProps(props: GenericParams): props is BProps
{
if ("b" in props && typeof props.b === "string")
return true;
return false;
}
Upvotes: 0
Reputation: 10994
Provided you have installed @types/react
and @types/react-dom
you will have most react types already defined.
For Function Components there is the type FunctionComponent
or FC
for short. It is also generic so you can specify the props type.
import type { FC } from 'react';
const A: FC<{ a: number }> = ({ a }) => <>{a}</>;
const B: FC<{ b: number }> = ({ b }) => <>{b}</>;
type Data = {
[id: string]: FC<any>;
};
const data: Data = {
aa: A,
bb: B,
};
const Usage = () => (
<>
A: {data.aa({ a: 12 })}
B: {data.bb({ b: 'hi' })}
</>
);
demo: https://stackblitz.com/edit/react-ts-y9ejme?file=App.tsx
Note however you lose the props type by typing an object as Data
, because you're allowing any props types. Typescript will not see that b
is supposed to be a number, not a string. It would be better to exclude that type and just let typescript infer the type from the object literal.
const A: FC<{ a: number }> = ({ a }) => <>{a}</>;
const B: FC<{ b: number }> = ({ b }) => <>{b}</>;
const data = {
aa: A,
bb: B,
};
export default function App() {
return (
<>
A: {data.aa({ a: 12 })}
{/* Error: Type 'string' is not assignable to type 'number'. */}
B: {data.bb({ b: 'hi' })}
</>
);
}
demo: https://stackblitz.com/edit/react-ts-j8uagi?file=App.tsx
Otherwise, you'll have to explicitly type the properties:
const A: FC<{ a: number }> = ({ a }) => <>{a}</>;
const B: FC<{ b: number }> = ({ b }) => <>{b}</>;
type Data = {
aa: typeof A;
bb: typeof B;
};
const data: Data = {
aa: A,
bb: B,
};
export default function App() {
return (
<>
A: {data.aa({ a: 12 })}
{/* Error: Type 'string' is not assignable to type 'number'. */}
B: {data.bb({ b: 'hi' })}
</>
);
}
demo: https://stackblitz.com/edit/react-ts-ykbq8q?file=App.tsx
Or ensure that the properties you are accessing are common for all components:
const A: FC<{ a: number }> = ({ a }) => <>I'm an A {a}</>;
const B: FC<{ a: number, b: number }> = ({ a, b }) => <>I'm a B {a}</>;
type Data = {
[id: string]: FC<{ a: number }>;
};
const data: Data = {
aa: A,
bb: B,
};
export default function App() {
return (
<>
A: {data.aa({ a: 12 })}
{/* Error: Type 'string' is not assignable to type 'number'. */}
B: {data.bb({ a: 'hi' })}
{/* You can pass b here, but it won't be typechecked */}
</>
);
}
demo: https://stackblitz.com/edit/react-ts-atcwwh?file=App.tsx
There's other options, but it really depends on your use case.
Upvotes: 1