Great work on this Anthony! This is a gnarly problem, and your approach really helped my team get this working in a more elegant way than we had done it before.
I've got a couple of suggestions to improve this that will help you to get rid of the type guards and allow support for forwardRefs.
Suggestion #1
You should consider replacing JSX.IntrinsicElements['button'] with React.ButtonHTMLAttributes<HTMLButtonElement> and JSX.IntrinsicElements['a'] with React.AnchorHTMLAttributes<HTMLAnchorElement>. This will allow you to support forwardRefs.
Suggestion #2
You don't need the type guards if you spread the props inside of if statements where the TypeScript compiler knows the value of the as prop.
if (props.as==='externalLink'){// Now TypeScript can infer that the rest props are all for an externalLink.const{as,...rest}=props;}
Here it is all together.
import*asReactfrom'react'import{Link}from'react-router-dom'importtype{LinkProps}from'react-router-dom'typeBaseProps={children:React.ReactNodeclassName?:stringstyleType:'primary'|'secondary'|'tertiary'}typeButtonAsButton=BaseProps&Omit<React.ButtonHTMLAttributes<HTMLButtonElement>,keyofBaseProps>&{as?:'button'}typeButtonAsUnstyled=Omit<ButtonAsButton,'as'|'styleType'>&{as:'unstyled'styleType?:BaseProps['styleType']}typeButtonAsLink=BaseProps&Omit<LinkProps,keyofBaseProps>&{as:'link'}typeButtonAsExternal=BaseProps&Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>,keyofBaseProps>&{as:'externalLink'}typeButtonProps=|ButtonAsButton|ButtonAsExternal|ButtonAsLink|ButtonAsUnstyledexportfunctionButton(props:ButtonProps):JSX.Element{constallClassNames=`${styleType?styleType:''}${className?className:''}`if (rest.as==='link'){const{allClassNames,...rest}=props;return<LinkclassName={allClassNames}{...rest}/>}elseif (as==='externalLink'){const{allClassNames,...rest}=propsreturn (<aclassName={allClassNames}// provide good + secure defaults while still allowing them to be overwrittentarget='_blank'rel='noopener noreferrer'{...rest}>{rest.children}</a>)}elseif (as==='unstyled'){const{className,...rest}=propsreturn<buttonclassName={className}{...rest}/>}else{const{allClassNames,...rest}=propsreturn<buttonclassName={allClassNames}{...rest}/>}thrownewError('could not determine the correct button type')}typeOmitFromTypes='className'|'styleType'|'as'
For further actions, you may consider blocking this person and/or reporting abuse
We're a place where coders share, stay up-to-date and grow their careers.
Great work on this Anthony! This is a gnarly problem, and your approach really helped my team get this working in a more elegant way than we had done it before.
I've got a couple of suggestions to improve this that will help you to get rid of the type guards and allow support for forwardRefs.
Suggestion #1
You should consider replacing
JSX.IntrinsicElements['button']
withReact.ButtonHTMLAttributes<HTMLButtonElement>
andJSX.IntrinsicElements['a']
withReact.AnchorHTMLAttributes<HTMLAnchorElement>
. This will allow you to support forwardRefs.Suggestion #2
You don't need the type guards if you spread the props inside of if statements where the TypeScript compiler knows the value of the as prop.
Here it is all together.