Wrapper function for JSX elements, typed via generics - reactjs

Currently this function works fine
function wrapElement(elem: JSX.Element) {
return ({ ...props }) => React.cloneElement(elem, { ...props })
}
I use it like this, because this way I can get intelliSense for tailwind classes
const Btn = wrapElement(<button className="[A LOT OF TAILWIND UTILITY CLASSES]" />)
But I'm trying to get it to return same type as it receives, so I could get intelliSense for attributes on intrinsic HTML elements.
Right now inferred type is
function wrapElement(elem: JSX.Element): ({ ...props }: {
[x: string]: any;
}) => React.FunctionComponentElement<any>.FunctionComponentElement<any>
I tried some stuff and it all failed with all kinds of errors, at this point it feels like this could be to hacky, but maybe I don't understand something?

It is basically impossible to get the correct props from a JSX.Element. You can achieve the design that you want, but you should pass in the element name and the props as separate arguments rather than passing in a JSX.Element.
This code can accept an element name like 'button' or any React component. It returns a function component with the same props. I am not dropping any props from the returned component because it seems like you are using this for setting defaults rather than dropping requirements.
import React, { ComponentType, ComponentProps } from "react";
const wrapElement = <
C extends keyof JSX.IntrinsicElements | ComponentType<any>
>(
Component: C,
presetProps: Partial<ComponentProps<C>>
) => (props: ComponentProps<C>) => {
const merged: ComponentProps<C> = { ...presetProps, ...props };
return <Component {...merged} />;
};
const Btn = wrapElement("button", {
className: "[A LOT OF TAILWIND UTILITY CLASSES]"
});
const Dbl = wrapElement(Btn, { onClick: () => alert("clicked") });
const Test = () => {
return <Dbl>Click</Dbl>;
};
Typescript Playground Link
You might want to customize your merge behavior to combine className or style properties rather than overriding them.
Note: when I tried to merge the props inline like <Component {...presetProps} {...props} /> I got a weird error "Type Partial<ComponentProps<C>> & ComponentProps<C> is not assignable to type IntrinsicAttributes & LibraryManagedAttributes<C, any>." So that is why I am merging the props on a separate line and annotating the type as ComponentProps<C> instead of the inferred type Partial<ComponentProps<C>> & ComponentProps<C>.

Related

Hide generic type prop on export

I have a small set of components in which a Wrapper is going to manipulate its children (therefore referred to as Components by injecting a prop into each of the children via cloneElement.
The gotcha here is that Component props are of a generic type. When I expose Component on the code, I don't want one of its props to be on the signature, because it will be automatically injected by the Wrapper component.
I have a concise example which shows what I mean:
types.ts
export type SomeObject = {
someKey: string;
};
type PropThatWillBeInjected<T extends SomeObject> = {
fn: (value: string) => T;
};
export type WannaBePropTypes = {
name: string;
};
export type PropTypes<T extends SomeObject> = PropThatWillBeInjected<T> &
WannaBePropTypes;
Important: PropTypes<T> is what Component expects, but as a programmer, I want WannaBePropTypes to be the signature of this component.
Moving on...
Component.tsx
function Component<T extends SomeObject>(props: PropTypes<T>) {
const { fn, name } = props;
const result = fn(name);
return <div>Hello, {result.someKey}</div>;
}
export default Component;
Wrapper.tsx
function Wrapper(props: { children: ReactNode }) {
const { children } = props;
return (
<div id="wrapper">
{React.Children.map(
children as ReactElement<PropTypes<SomeObject>>,
(child, index) =>
cloneElement(child, {
...child.props,
fn: (value: string) => ({
someKey: `${value}-${index}`,
}),
})
)}
</div>
);
}
export default Wrapper;
As expected, when I try to use these components as the following, the code works but the compiler complains:
<Wrapper>
<Component name="Alice" />
<Component name="Bob" />
</Wrapper>
Property 'fn' is missing in type '{ name: string; }' but required in type 'PropThatWillBeInjected'.(2741)
Is there a way to cast Component so I don't need to pass fn manually? I know there's a way when the prop types is not generic...
What I've tried:
Making fn optional: works, but this is not the solution I'm looking for;
Wrapping Component with another component and passing a noop to Component: works, but I don't want to create this unnecessary wrapper;
A playground with this sample code: StackBlitz
If I inderstand your problem correctly, you want to call Component as <Component name="Alice" /> and there should be some internal logic for two cases: when fn was passed and when not. If so, you can create unnecessary type (instead of unnecessary wrapper) which will be one of WannaBePropTypes or full props. This is like some combination of your try#1 and try#2:
type FullProps<T extends SomeObject> = PropThatWillBeInjected<T> & WannaBePropTypes;
type PropTypes<T extends SomeObject> = FullProps<T> | WannaBePropTypes;
So fn is optional until you define children as ReactElement<FullProps<SomeObject>> in Wrapper component. This is how to tackle with Typescript only.
BTW: maybe you can just pass array of WannaBePropTypes objects into Wrapper instead of children? This sounds better if <Component name="Alice" /> should do nothing by itself.

extend inline typescript interface on function

I have a pretty simple React component which takes a required prop, active.
Setting the inline interface for this one prop is quite simple. But now, I need to account for the other types of props that can be passed onto the component.
Luckily, Native supplies the props for this element, so I don't have to recreate the interface for it. But I'm struggling to figure out how to extend my current inline interface to get it to work.
I've tried this approach:
const BarTabName = ({ active, ...props }: { active: boolean; } extends TextProps) => {
return <Text {...props} />;
};
But this just results in the error:
'?' expected.ts(1005)
If possible, I'm trying to avoid creating a separate interface outside of the parameters.
Use an intersection:
const BarTabName = ({ active, ...props }: { active: boolean; } & TextProps) => {

Constraint child elements

I have a Modal function component and it should have three child function components Header, Body and Footer and I want to constrain the Modal to only allow elements of type Header | Body | Footer as it's top level child elements.
<Modal>
<Modal.Header></Modal.Header>
<Modal.Body></Modal.Body>
<Modal.Footer></Modal.Footer>
</Modal>
I have created the function components but I don't know how to constraint them:
import React, { ReactElement, ReactNode } from 'react'
function Header(props: { children: ReactElement }) {
const { children } = props
return <>{children}</>
}
function Body(props: { children: ReactElement }) {
const { children } = props
return <>{children}</>
}
function Footer(props: { children: ReactElement }) {
const { children } = props
return <>{children}</>
}
function Modal(props: { children }) {
const { children } = props
return <>{children}</>
}
Modal.Header = Header
Modal.Body = Body
Modal.Footer = Footer
export default Modal
I tried to create a return type for the functions like this (example for Header) but it didn't prevent to pass e.g. a <div/> instead of the Header element:
type Header = ReactElement
function Header(props: { children: ReactElement }): Header => {...}
function Modal(props: { children: Header }): ReactElement => {...}
I've created a TypeScript Playground here.
TL;DR: This is currently not possible due to the typing of JSX elements in TypeScript. Read on to get some more detail, if you like.
The culprit
From the TS JSX Docs:
The JSX result type
By default the result of a JSX expression is typed as any. You can customize the type by specifying the JSX.Element interface. However, it is not possible to retrieve type information about the element, attributes or children of the JSX from this interface. It is a black box.
The issue pretty much all other issues I found on the topic (like this one) link to is open since February 2018 and still active.
Down the typing rabbit hole
The react typings define JSX.Element as a React.ReactElement<any, any>. The fact aside that any, any doesn't look promising in the first place, let's see if we can make use of ReactElement in any way to lock down typing any further to help us with typing our children further (we know the answer is no but let's see why that's the case).
Thanks to GitHub's excellent indexing of the React typings, we can easily find the React.ReactElement<P, T> definition here:
interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
type: T;
props: P;
key: Key | null;
}
telling us that the first type parameter is for the props (so that isn't useful unless we wanted to try and type check based on props, see the alternatives paragraph below). The second parameter sends us down a little further; type: T sounds interesting and is constrained to string | JSXElementConstructor<any>. Maybe this gets us further?
Answer is no once more. After finding the definition:
type JSXElementConstructor<P> =
| ((props: P) => ReactElement<any, any> | null)
| (new (props: P) => Component<any, any>);
it turns out that the any in JSXElementConstructor<any> also stands for some sort of props.
For completeness sake, looking at the the definition of Component we will find that it takes the types of the class component's props and state as type arguments.
I tried to work back from there and type Modal's child with something like
type ModalChild = ReactElement<unknown, new (props: unknown) => MHeader>;
function Modal(props: { children: ModalChild | ModalChild[] }) {
const { children } = props
return <>{children}</>
}
however that still somehow allowed spans and divs as children.
There is not much further to go from here so it seems the statement from the JSX docs holds true as well for the specific case of React types.
(Non-)Alternatives
There is a related question on the site which talks about the same issue: How do I restrict the type of React Children in TypeScript, using the newly added support in TypeScript 2.3?. Existing answers suggest typechecking through the props the components take, which however I couldn't get to work (and both high voted answers note that there is no proper typechecking either). I still wanted to mention the idea of this approach for completeness sake.
I am surprised I would ever say this but in this case, Flow has an advantage over TypeScript in that its typings for React (specifically React.Element) take a generic parameter defining the type of the component, thus allowing only specific items. See their docs page on the topic typing their props as
type Props = {
children: React.ChildrenArray<React.Element<typeof TabBarIOSItem>>,
};
limiting the children of the TabBarIOS to TabBarIOSItems.
Your Modal can iterate over its children and discard any disallowed content and log a warning.
const Header = props => props.children;
const Body = props => props.children;
const Footer = props => props.children;
const Modal = ({ children }) => {
const content = [];
React.Children.forEach(children,
(child) => {
if ([Header, Body, Footer].includes(child.type))
content.push(child);
else
console.warn(`child elements of type ${child.type} not allowed as content`);
}
);
return content;
};
Modal.Header = Header;
Modal.Body = Body;
Modal.Footer = Footer;

TypeScript error "not assignable to type 'IntrinsicAttributes'" for React component with extended discriminated union

Problem:
I have an array of objects whose interfaces all extend a base interface.
I want to map these objects onto a React component which will route to specialised components for each of the supported child interfaces -- but first, I want to map over the array and extend each object with an onClick handler, whose signature is a generic which I want to specialise to suit whichever child interface it's being mapped onto.
I've come up with a solution that looks as though it should work, but I can't shake this TS error: Type 'AWithClick<T>' is not assignable to type 'IntrinsicAttributes. I see plenty of references in SO and elsewhere to TS errors related to that interface, but none quite seems to apply here.
I checked my solution against this helpful article, and I think the main difference in my implementation is that I'm trying to extend items from the union with specialised onClicks, rather than defining the specialised onClicks in the individual interfaces in the original union. The array of objects comes from a store, and I'm basically mapping its entities to component props, so I want to keep component props interfaces separate from the store interfaces.
Steps to repro:
npx create-react-app repro --template typescript
Replace App.tsx with the following:
import React from 'react';
enum AType { 'as', 'an' }
interface A {
type: AType;
}
interface AS extends A {
type: AType.as;
value: string;
}
interface AN extends A {
type: AType.an;
value: number;
}
type AnyA = AS | AN;
type AWithClick<T extends AnyA> = T & { onClick: (value: T['value']) => void }
const ASComponent = (props: AWithClick<AS>) => <button onClick={() => props.onClick(props.value)}>{props.value}</button>;
const ANComponent = (props: AWithClick<AN>) => <button onClick={() => props.onClick(props.value)}>{props.value}</button>;
const TestComponent = <T extends AnyA>(props: AWithClick<T>) => {
switch (props.type) {
case AType.as: return <ASComponent {...props} />;
case AType.an: return <ANComponent {...props} />;
}
};
const withoutClicks = [
{ type: AType.as, value: 'AS!' } as AS,
{ type: AType.an, value: 1 } as AN,
];
const withClicks = withoutClicks.map(<T extends AnyA>(props: T) => ({
...props,
onClick: (value: T['value']) => { console.log(value) },
}));
const TestComponentMain = () =>
<div>
{withClicks.map(props => <TestComponent {...props} key={props.type} />)}
</div>
export default TestComponentMain;
Now npm start and you'll see the error Type 'AWithClick<T>' is not assignable to type 'IntrinsicAttributes
It seems that typescript can't quite follow the logic to know that you are refining the type adequately.
But TestComponent here does not need to be generic. You can simply declare your argument as the superset of what you support, and then refine the type with conditionals.
This works:
const TestComponent = (props: AWithClick<AnyA>) => {
switch (props.type) {
case AType.as: return <ASComponent {...props} />;
case AType.an: return <ANComponent {...props} />;
}
};
Playground
In general, when troubleshooting error message with generics, it's always good ask the question "does this really need to be generic?"

Currying styled Template Literal function

Im trying to curry the styled function but react seems to throwing an error at run time. (JSX element does not have any constructor or call signatures)
The idea is to create a base style that can be reused (without setting the element its attached to).
const template = baseStyle`
backgroundColor: reg
`
const Component1 = template(div)
const Component2 = template(a)
The function accepts an html or styled component. This is what i've done so far, im pretty sure the types are off.
export const baseStyle = (strings: TemplateStringsArray, ...rest: Interpolation<any>[]) => {
return (element: AnyStyledComponent): StyledComponent<any, any, any, any> => {
return styled(element)(strings, ...rest)
}
}
Is there a way to do this?
What you are trying to do here is tough because the types for styled-components are really complex. Right now your element argument is AnyStyledComponent which is an element that's already styled.
This type seems to work, but note that styles from the template override those from an already-styled component:
export const baseStyle = (
strings: TemplateStringsArray,
...rest: Interpolation<any>[]
) => {
return <C extends keyof JSX.IntrinsicElements | React.ComponentType<any>>(element: C): StyledComponent<C, any, {}, never> => {
return styled(element)(strings, ...rest);
};
};
You might have better luck using the css utility.

Resources