Currying styled Template Literal function - reactjs

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.

Related

How to type custom `preload` method for React.lazy components?

When trying to implement a preload() method for React.lazy() components, a typical pattern looks something like,
const ReactLazyPreload = (importStatement) => {
const Component = React.lazy(importStatement);
Component.preload = importStatement; // Property 'preload' does not exist on type 'LazyExoticComponent<T>'.
return Component;
};
which can later be used, eg,
const MyComponent = ReactLazyPreload(() => import("./MyComponent.tsx");
const onHover = () => { MyComponent.preload() };
However, the assignment on the 3rd line of the first snippet causes a TS error,
Property 'preload' does not exist on type 'LazyExoticComponent<T>'.
I've been playing around with declare, but have been unsuccessful in removing the error. What type should be used for the preload() method?
// extend lazy component with `preload` property
interface LazyPreload<Props>
extends React.LazyExoticComponent<React.ComponentType<Props>> {
preload: () => {};
}
function ReactLazyPreload<Props>(
importStatement: () => Promise<{ default: React.ComponentType<Props> }>
) {
// use Object.assign to set preload
// otherwise it will complain that Component doesn't have preload
const Component: LazyPreload<Props> = Object.assign(
React.lazy(importStatement),
{
preload: importStatement,
}
);
return Component;
}
TypeScript is trying to prevent you from making a mistake here.
Just because other people follow a convention doesn't make it a good one: in this case, it's not a safe one. As a general rule, it's never safe to mutate things you don't own.
While I can't find anything in the React codebase at the current version tag (17.0.2) that would seem to cause an issue with assigning something to the preload property of a lazy component, that doesn't mean that the React maintainers won't use this property in a subsequent release. If that happens, and you overwrite that property, then unpredictable behavior would arise.
Instead of mutating the component, just return the preload function alongside it:
TS Playground link
import {default as React, lazy} from 'react';
import type {ComponentType, LazyExoticComponent} from 'react';
export type ReactLazyFactory<T = any> = () => Promise<{default: ComponentType<T>}>;
export type ComponentPreloadTuple<T = any> = [
component: LazyExoticComponent<ComponentType<T>>,
preloadFn: () => void,
];
export function getLazyComponentWithPreload <T = any>(componentPath: string): ComponentPreloadTuple<T>;
export function getLazyComponentWithPreload <T = any>(factory: ReactLazyFactory<T>): ComponentPreloadTuple<T>;
export function getLazyComponentWithPreload <T = any>(input: string | ReactLazyFactory<T>): ComponentPreloadTuple<T> {
const factory = () => typeof input === 'string' ? import(input) : input();
return [lazy(factory), factory];
}
// ----------
// Example.tsx
export type ExampleProps = {
text: string;
};
export default function ExampleComponent ({text}: ExampleProps) {
return <div>{text}</div>;
}
// ----------
// AnotherComponent.tsx
// use with path to component:
const [Example1, preloadExample1] = getLazyComponentWithPreload<ExampleProps>('./Example');
// use with factory function:
const [Example2, preloadExample2] = getLazyComponentWithPreload<ExampleProps>(() => import('./Example'));

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;

Wrapper function for JSX elements, typed via generics

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>.

Styled components in a hoc react component

I am having two issues with using styled components in a hoc wrapper in react.
The component is rendered, but not with the background color.
The ComponentWithAddedColors is not valid typescript. Don't know why.
Anyone who can help with this?
interface IProps {
id: string;
left: number;
top: number;
}
export const Node: React.FC<IProps> = ({ id, left, top }) => {
return (
<Container left={left} top={top}>
{id}
</Container>
);
};
function withColors<T>(Component: React.ComponentType<T>) {
const bg = "hotpink";
const ComponentWithAddedColors = styled(Component)`
${bg && `background: ${bg};`}
`;
const result: React.FC<T> = (props) => (
<ComponentWithAddedColors {...props} />
);
return result;
}
const DraggableNode = withColors(Node);
export default DraggableNode;
I have made a code sandbox to illustrate the issue:
https://codesandbox.io/s/styled-hoc-xgduo?file=/src/Components/Node/Node.tsx
Style Errors Explained
#Mosh Feu's comment pointed me in the right direction.
You can add styles to an already styled component and you can add styles to a custom component, but those two things work differently. You have a chain that goes through both types, so things are getting lost.
When you call withColors(Node) what this is doing is passing a generated className prop to Node. But your custom component Node never does anything with this prop, so the style is never applied.
The styled method works perfectly on all of your own or any third-party component, as long as they attach the passed className prop to a DOM element.
Style Errors Fixed
If we edit Node to use this className, we get the color!
export const Node: React.FC<IProps & {className?: string}> = ({ id, left, top, className}) => {
return (
<Container left={left} top={top} className={className}>
{id}
</Container>
);
};
TS Errors Explained
As far as the typescript errors are concerned, you're getting an error about assigning your props T to the props of a styled component (ComponentWithAddedColors), which shows up as a bunch of crazy nonsense:
(props: (Pick<Pick<(PropsWithoutRef & RefAttributes<Component<T, any, any>>) | (PropsWithRef<PropsWithChildren> & {}), Exclude<...> | ... 1 more ... | Exclude<...>> & Partial<...>, Exclude<...> | ... 1 more ... | Exclude<...>> & { ...; } & { ...; }) | (Pick<...> & ... 2 more ... & { ...; })): ReactElement<...>
This is mainly because of ref forwarding through the ForwardRefExoticComponent type.
But we can work backwards to get the expected props type from the component type using a utility type:
type PropsOf<T> = T extends React.ComponentType<infer P> ? P : never;
So ComponentWithAddedColors has props PropsOf<typeof ComponentWithAddedColors>. We could use that, but we also know that ComponentWithAddedColors has type StyledComponent<React.ComponentType<T>, any, {}, never>, so we can go back a step further:
type StyledProps<InitialProps> = PropsOf<StyledComponent<React.ComponentType<InitialProps>, any, {}, never>>
So ComponentWithAddedColors has props StyledProps<T>.
TS Errors Fixed
That said, all of this is unnecessary, at least in the example you've shown. You are passing all of the props of ComponentWithAddedColors through to ComponentWithAddedColors, so result is the same thing as the component itself. Just return it directly.
function withColors<T>(Component: React.ComponentType<T>) {
const bg = "hotpink";
return styled(Component)`
${bg && `background: ${bg};`}
`;
}

Typescript with React - use HOC on a generic component class

I have a generic React component, say like this one:
class Foo<T> extends React.Component<FooProps<T>, FooState> {
constructor(props: FooProps<T>) {
super(props);
render() {
return <p> The result is {SomeGenericFunction<T>()}</p>;
}
}
I also have a HOC that looks similar to this one (but is less pointless):
export const withTd =
<T extends WithTdProps>(TableElement: React.ComponentType<T>): React.SFC<T> =>
(props: T) => <td><TableElement {...props}/></td>;
But when I use a component like this:
const FooWithTd = withTd(Foo);
There is no way to pass the type argument, as you can do neither withTd(Foo<T>), nor can you do FooWithTd, the type is always wrong.
What is the proper way to do that?
EDIT: The problem is that I want to be able to have something like <FooWithTd<number> {...someprops}/> later on, as I don't know the desired type for T in the HOC.
You can wrap your component which is created from a HOC into another component. It would look something like this:
class FooWithTd<T> extends React.Component<SomeType<T>> {
private Container: React.Component<SomeType<T> & HOCResultType>;
constructor(props:SomeType<T>){
super(props);
this.Container = withTd(Foo<T>);
}
render() {
return <this.Container {...this.props} />;
}
}
Remember, you probably don't want the HOC inside your render function because it means that the component will be recreated every each render.
Thanks for asking this question. I just figured out a way to specify a type parameter to a component after wrapping it with an HOC and I thought I'd share.
import React from 'react';
import withStyles from '#material-ui/core/styles/withStyles';
import { RemoveProps } from '../helpers/typings';
const styles = {
// blah
};
interface Props<T> {
classes: any;
items: T[];
getDisplayName: (t: T) => string;
getKey: (t: T) => string;
renderItem: (t: T) => React.ReactNode;
}
class GenericComponent<T> extends React.Component<Props<T>, State> {
render() {
const { classes, items, getKey, getDisplayName, renderItem } = this.props;
return (
<div className={classes.root}>
{items.map(item => (
<div className={classes.item} key={getKey(item)}>
<div>{getDisplayName(item)}</div>
<div>{renderItem(item)}</div>
</div>
))}
</div>
);
}
}
// 👇 create a `type` helper to that output the external props _after_ wrapping it
type ExternalProps<T> = RemoveProps<Props<T>, 'classes'>;
export default withStyles(
styles
)(GenericComponent) as <T extends any>(props: ExternalProps<T>) => any;
// 👆 cast the wrapped component as a function that takes
// in a type parameter so we can use that type
// parameter in `ExternalProps<T>`
The main idea is to cast the wrapped component as a function that takes in a type parameter (e.g. T) and use that type parameter to derive the external props after the component has been wrapped.
If you do this, then you can specify a type parameter when using the wrapped version of GenericComponent e.g.:
<GenericComponent<string> {/*...*/} />
Hopefully the code is explanatory enough for those who still have this problem. In general though, I consider this relatively advanced typescript usage and it's probably easier to use any instead of a generic parameter in the props
Workaround: simple case
If your component's type parameter is used only for passing it to props, and users of the component do not expect it having any functionality beyond just passing props and rendering, you can explicitly hard-cast the result of your hoc(...args)(Component) to React's functional component type, like this:
import React, {ReactElement} from 'react';
class MyComponent<T> extends React.Component<MyProps<T>> { /*...*/ }
const kindaFixed = myHoc(...args)(MyComponent) as unknown as <T>(props: MyProps<T>) => ReactElement;
Workaround: more complex and with some runtime costs
You can use fabric-like function, supposed here:
class MyComponent<T> extends React.Component<MyProps<T>> { /*...*/ }
export default function MyComponentFabric<T>() {
return hoc(...args)(MyComponent as new(props: MyProps<T>) => MyComponent<T>);
}
This one will require you to create new version of wrapped component for each type you use it with:
import MyComponentFabric from '...whenever';
const MyComponentSpecificToStrings = MyComponentFabric<string>();
It will allow you to access all public instance fields and methods of your component.
Summary
I faced this issue when tried to use connect from react-redux on my ExampleGenericComponent<T>. Unfortunatelly, it cannot be fixed properly until TypeScript will support HKT, and any HOC you use will update its typings respecting this feature.
There is possibly no correct solution (at least for now) for usages beyond just rendering, when you need to access component instance fields and methods. By 'correct' I mean 'without ugly explicit typecasts', and 'with no runtime cost'.
One thing you can try is to split your class-component into two components, one that will be used with HOC, and other that will provide fields and methods that you need.
Just stumbled upon this as well and thought I'd share what I came up with in the end.
Building on what #rico-kahler provided, my approach mapped to your code would be
export const FooWithTd = withTd(Foo) as <T>(props: FooProps<T>) => React.ReactElement<FooProps<T>>;
which you can then use like this
export class Bar extends React.Component<{}> {
public render() {
return (
<FooWithTd<number> />
);
}
}
In my case, I have defaultProps as well and I inject props by ways of another HOC, the more complete solution would look like this:
type DefaultProps = "a" | "b";
type InjectedProps = "classes" | "theme";
type WithTdProps<T> = Omit<FooProps<T>, DefaultProps | InjectedProps> & Partial<FooProps<T> & { children: React.ReactNode }>;
export const FooWithTd = withTd(Foo) as <T>(props: WithTdProps<T>) => React.ReactElement<WithTdProps<T>>;
EDIT:
After some changes to your code, it was only a wrong constraint T in your withTd function.
// I needed to change the constraint on T, but you may adapt with your own needs
export const withTd = <T extends FooProps<WithTdProps>>(
TableElement: React.ComponentType<T>
): React.SFC<T> => (props: T) => (
<td>
<TableElement {...props} />
</td>
)
// Explicitly typed constructor
// Removed after EDIT
//const FooW = Foo as new (props: FooProps<WithTdProps>) => Foo<WithTdProps>
// Inferred as React.StatelessComponent<FooProps<WithTdProps>>
const FooWithTd = withTd(Foo)
No longer relevant after EDIT :
You may find more information at this issue https://github.com/Microsoft/TypeScript/issues/3960

Resources