Reputation: 3806
I', trying to create a react component which emulates a native html element.
Allowing a valid html attribute
Example:
type TAtt = {
[key: string]: React.HTMLAttributes<HTMLElement>;
};
const NavAnyValidAttribute: React.FC<TAtt> = props => {
const { children, ...therest } = props;
return <nav {...therest}>{children}</nav>;
};
<NavAnyValidAttribute className="x">some children</NavAnyValidAttribute>;
Type 'string' has no properties in common with type 'HTMLAttributes'.(2559) input.tsx(135, 3): The expected type comes from this index signature.
type TProps = TAtt & {
TagName: string; //keyof JSX.IntrinsicElements - also doesn't work!;
}
const Tag: React.FC<TProps> = ({ TagName, children, ...props }) =>
React.createElement(TagName, props, children);
const Nav = <Tag {...{ TagName: 'nav' }} />;
Error: Type '{ TagName: string; }' is not assignable to type 'TAtt'. Property 'TagName' is incompatible with index signature. Type 'string' has no properties in common with type 'HTMLAttributes'.ts(2322)
Any help appreciated. thanks
Upvotes: 1
Views: 3891
Reputation: 3806
Ok I have created a solution.
I'm sure there is a better way of doing it but in order to keep this strict, and allow passing this component to another unknown component handler I had to ensure I returned the Component<any>.
I also had to ensure I had a strict TagName: React.ElementType. Or "keyof JSX.IntrinsicElements". Not sure which is more appropriate. But the same has to be used for both methods as below.
I also had to return the method GetTagAsComp as a member of the same component TagAsComp.
If anyone has a tidier approach feel free to add.
import React from 'react';
type TElem = React.ElementType; // OR keyof JSX.IntrinsicElements;
// Might have to extend this for specific element attributes - see
// - https://www.saltycrane.com/cheat-sheets/typescript/react/latest/
type TAtt = React.HTMLAttributes<HTMLElement>;
type TCreateElement = {
TagName: TElem;
props: TAtt;
};
const CreateElement: React.FC<TCreateElement> = ({ TagName, props, children }) =>
React.createElement(TagName, props, children);
// technically this is a hoc, so would use prefix - with
// but it doesnt name well
// This existing name: TagAsComp is better describing what it is in shorthand - Tag as a component.
const TagAsComp = (TagName: TElem): React.ComponentType<any> => {
const GetTagAsComp: React.FC<TAtt> = props => {
const { children } = props;
return <CreateElement {...{ TagName, props, children }} />;
};
return GetTagAsComp;
};
export default TagAsComp;
// Usecase:
// const Tag = TagAsComp('div')
// <Tag onClick={doThing} className="hello" data-x="extra"> Some children </Tag>
For my usecase I wanted to pass it to styled components like this: See my other post:
const Nav = Style(TagAsComp('div'), theme);
The above solution was not needed, though it answer my original question and maybe beneficial for other use cases.
I would prefer this now as its more elegant:
const StyleNav = (props: TTheme): StyledComponent<'nav', {}> => styled.nav`
background: ${props.grey1};
display: block;
`;
Upvotes: 1