So I have been playing around with type systems in JavaScript and for the most part things are working however there is an issue with styled-components. I can't seem to find a good way to apply flow to the props of a styled-component. So far the only solution I see is:
export type ButtonPropTypes = ReactPropTypes & {
styleType: 'safe' | 'info' | 'warning' | 'danger' | 'link',
isPill: boolean,
isThin: boolean,
};
export const ButtonStyled = styled.button`
${generateBaseStyles}
${hoverStyles}
${fillStyles}
${thinStyles}
${linkStyles}
`;
export const Button = (props: ButtonPropTypes) => <ButtonStyled {...props} />;
It seems pretty excessive that I have to create 2 component for every styled component.
I am hoping my google skills are just crap and I am missing something but is there a better way to do this other than multiple components per styled component?
Yes! There is a better way. The trick is to declare the type of the component created by styled-components. You can do this by casting the result returned by styled.button`...` to the type of a React component that takes in your desired props. You can generate the type of a React component that takes in arbitrary props with type mytype = React.ComponentType<MyProps>.
// #flow
import styled from 'styled-components'
// Make sure you import with * to import the types too
import * as React from 'react'
// Mock function to use styleType
const makeStyles = ({styleType}) => ''
export type ButtonPropTypes = {
styleType: 'safe' | 'info' | 'warning' | 'danger' | 'link',
isPill: boolean,
isThin: boolean,
};
export const ButtonStyled = (styled.button`
${makeStyles}
${({isPill}) => isPill ? 'display: block;' : ''}
${({isThin}) => isThin ? 'height: 10px;' : 'height: 100px;'}
`: React.ComponentType<ButtonPropTypes>) // Here's the cast
const CorrectUsage = <ButtonStyled styleType="safe" isPill isThin/>
const CausesError = <ButtonStyled styleType="oops" isPill isThin/> // error
const CausesError2 = <ButtonStyled styleType="safe" isPill="abc" isThin={123}/> // error
I've hosted the code on GitHub for local reproduction (since Flow's sandbox doesn't work with external dependencies): https://github.com/jameskraus/flow-example-of-styled-components-props
In addition to James Kraus' answer, if you're using flow-typed (and have installed the package for your version of styled-components) you can essentially:
import styled, {type ReactComponentStyled} from 'styled-components'
type Props = {
color?: string
}
const Button: ReactComponentStyled<Props> = styled.button`
color: ${({color}) => color || 'hotpink'};
`
Related
I working with framer-motion in my NextJS project. I'm trying to import {motion} using Next's dynamic import. But unfortunately, it doesn't seem to work.
import { motion } from "framer-motion"
I'm trying to convert the above import as a dynamic import as given below:
const motion = dynamic(() =>
import("framer-motion").then((module) => module.motion)
)
But it throws an error :
"Argument of type '() => Promise<ComponentClass<never, any> | FunctionComponent<never> | { default: ComponentType<never>; } | ((<Props>(Component: string | ComponentType<Props>, customMotionComponentConfig?: CustomMotionComponentConfig | undefined) => CustomDomComponent<...>) & HTMLMotionComponents & SVGMotionComponents)>' is not assignable to parameter of type 'DynamicOptions<{}> | Loader<{}>'."
Whenever I import other things like icons, custom components it works as expected, for example the dynamic import given below works fine :
const DoubleArrowRightIcon = dynamic(() => import("#radix-ui/DoubleArrowRightIcon"), {
loading: () => <p>..</p>,
})
I have looked at other answers and found this link but still, not able to make it work.
Any help please?
I want to extend the MUI5 components and also use their component prop. To do that according to the documentation I make it work in the below code:
import Button, { ButtonProps } from '#mui/material/Button';
import React from 'react';
export type MyButtonProps<C extends React.ElementType> = ButtonProps<C, { component?: C }> & {
myOtherField: string;
};
export function MyButton<C extends React.ElementType>(props: MyButtonProps<C>) {
const { myOtherField, ...buttonProps } = props;
return <Button {...buttonProps}></Button>;
}
However when I try to access anything belongs to MUI5 button attributes, vscode is not helping me to find it
see the below screenshot:
But after adding it manually it is not complaining about it.
see the below screenshot and check that it is inferring the type as any
However if I remove the <C extends React.ElementType> logic from the type it is working like a charm (but it has a drawback since it is now complaining about the component prop when I use it anywhere in my application):
What can be the problematic part in the first place when I am extending MUI5 button prop types?
thank you in advance.
This is the easy way I found for your usecase:
type NewButtonProps = ButtonProps & {
otherField: number;
component: React.ElementType;
};
function Button3({ otherField, ...props }: NewButtonProps) {
const y = props.onClick;
const x = otherField;
return <Button {...props}></Button>;
}
All the attributes is understand by typescript now:
I am building a library of components and I need some of them to have a customizable tag name. For example, sometimes what looks like a <button> is actually a <a>. So I would like to be able to use the button component like so:
<Button onClick={onClick}>Click me!</Button>
<Button as="a" href="/some-url">Click me!</Button>
Ideally, I would like the available props to be inferred based on the "as" prop:
// Throws an error because the default value of "as" is "button",
// which doesn't accept the "href" attribute.
<Button href="/some-url">Click me!<Button>
We might need to pass a custom component as well:
// Doesn't throw an error because RouterLink has a "to" prop
<Button as={RouterLink} to="/">Click me!</Button>
Here's the implementation, without TypeScript:
function Button({ as = "button", children, ...props }) {
return React.createElement(as, props, children);
}
So, how can I implement a "as" prop with TypeScript while passing down the props?
Note: I am basically trying to do what styled-components does. But we are using CSS modules and SCSS so I can't afford adding styled-components. I am open to simpler alternatives, though.
New answer
I recently came across Iskander Samatov's article React polymorphic components with TypeScript in which they share a more complete and simpler solution:
import * as React from "react";
interface ButtonProps<T extends React.ElementType> {
as?: T;
children?: React.ReactNode;
}
function Button<T extends React.ElementType = "button">({
as,
...props
}:
ButtonProps<T>
& Omit<React.ComponentPropsWithoutRef<T>, keyof ButtonProps<T>>
) {
const Component = as || "button";
return <Component {...props} />;
}
Typescript playground
Old answer
I spent some time digging into styled-components' types declarations. I was able to extract the minimum required code, here it is:
import * as React from "react";
import { Link } from "react-router-dom";
type CustomComponentProps<
C extends keyof JSX.IntrinsicElements | React.ComponentType<any>,
O extends object
> = React.ComponentPropsWithRef<
C extends keyof JSX.IntrinsicElements | React.ComponentType<any> ? C : never
> &
O & { as?: C };
interface CustomComponent<
C extends keyof JSX.IntrinsicElements | React.ComponentType<any>,
O extends object
> {
<AsC extends keyof JSX.IntrinsicElements | React.ComponentType<any> = C>(
props: CustomComponentProps<AsC, O>
): React.ReactElement<CustomComponentProps<AsC, O>>;
}
const Button: CustomComponent<"button", { variant: "primary" }> = (props) => (
<button {...props} />
);
<Button variant="primary">Test</Button>;
<Button variant="primary" to="/test">
Test
</Button>;
<Button variant="primary" as={Link} to="/test">
Test
</Button>;
<Button variant="primary" as={Link}>
Test
</Button>;
TypeScript playground
I removed a lot of stuff from styled-components which is way more complex than that. For example, they have some workaround to deal with class components which I removed. So this snippet might need to be customized for advanced use cases.
I found that you can make the same thing with JSX.IntrinsicElements. I have Panel element:
export type PanelAsKeys = 'div' | 'label'
export type PanelAsKey = Extract<keyof JSX.IntrinsicElements, PanelAsKeys>
export type PanelAs<T extends PanelAsKey> = JSX.IntrinsicElements[T]
export type PanelAsProps<T extends PanelAsKey> = Omit<PanelAs<T>, 'className' | 'ref'>
I omitted native types like ref and className because i have my own types for these fields
And this how props will look look like
export type PanelProps<T extends PanelAsKey = 'div'> = PanelAsProps<T> & {}
const Panel = <T extends PanelAsKey = 'div'>(props: PanelProps<T>) => {}
Then you can use React.createElement
return React.createElement(
as || 'div',
{
tabIndex: tabIndex || -1,
onClick: handleClick,
onFocus: handleFocus,
onBlur: handleBlur,
onMouseEnter: handleMouseEnter,
onMouseLeave: handleMouseLeave,
'data-testid': 'panel',
...rest
},
renderChildren)
I see no ts errors in this case and have completions for label props like htmlFor
In the Creating a component section of React Typescript Starter example, Creating a component, there is a basic React component in Typescript:
// src/components/Hello.tsx
import * as React from 'react';
export interface Props {
name: string;
enthusiasmLevel?: number;
}
function Hello({ name, enthusiasmLevel = 1 }: Props) {
if (enthusiasmLevel <= 0) {
throw new Error('You could be a little more enthusiastic. :D');
}
return (
<div className="hello">
<div className="greeting">
Hello {name + getExclamationMarks(enthusiasmLevel)}
</div>
</div>
);
}
export default Hello;
// helpers
function getExclamationMarks(numChars: number) {
return Array(numChars + 1).join('!');
}
I am new to Typescript. It seems that the interface Props is used by Typescript to do props type checks (similar to what the Proptypes npm package does). So the question is:
If I am already using this kind of Typescript
interface syntax do to props type check, do I still need to use
Proptypes package like this in the same component?
import PropTypes from 'prop-types';
Hello.propTypes = {
name: PropTypes.string,
enthusiasmLevel: PropTypes.number
};
Besides, why does here use export interface? what is the purpose
of exporting the interface Props? Is it compulsory?
Firstly I recommend declaring your components the ES6 way
const Hello: React.FC<IHello> = ({ name, enthusiasmLevel = 1 }) => {}
Your interface defines the contract of your component / The accepted parameters
export interface IHello {
name: string;
enthusiasmLevel?: number;
}
You are exporting this, so you can import your interface from other files / components which want to make use of the Hello component. For example you can use your Hello component like so from another component:
const props: IHello = {
name: "John",
enthusiamsLevel: 5
}
<Hello {...props} />
If I am already using this kind of Typescript interface syntax do to props type check, do I still need to use Proptypes in the same component?
You always want type strong definitions in TypeScript. So when declaring your prop variable in another component, you don't want to do const props: any = {
If you decide to change your interface declaration for this component later on, you would be forced to update all your references which uses this interface. - You might want to require 1 more prop variable and in that case you would want to update your usages of this interface. If you are not used to TypeScript this can seem quite hideous at first - but the benefit of always having strong type definitions will show over time. Especially when you update your type definitions.
I'm using styled-system and one key of the library is to use the shorthand props to allow easy and fast theming.
I've simplified my component but here is the interesting part:
import React from 'react'
import styled from 'styled-components'
import { color, ColorProps } from 'styled-system'
const StyledDiv = styled('div')<ColorProps>`
${color}
`
const Text = ({ color }: ColorProps) => {
return <StyledDiv color={color} />
}
I have an error on the color prop which says:
Type 'string | (string | null)[] | undefined' is not assignable to
type 'string | (string & (string | null)[]) | undefined'.
I think that's because styled-system use the same naming as the native HTML attribute color and it conflicts.
How do I solve this?
color seems to be declared in react's declaration file under HTMLAttributes - it's not exported.
I had to work around this by creating a custom prop
Example is using #emotion/styled but also works with styled-components
// component.js
import styled from '#emotion/styled';
import { style, ResponsiveValue } from 'styled-system';
import CSS from 'csstype';
const textColor = style({
prop: 'textColor',
cssProperty: 'color',
key: 'colors'
});
type Props = {
textColor?: ResponsiveValue<CSS.ColorProperty>
}
const Box = styled.div<Props>`
${textColor};
`
export default Box;
// some-implementation.js
import Box from '.';
const Page = () => (
<Box textColor={['red', 'green']}>Content in a box</Box>
);
This seems to only happen when you pass the prop down from an ancestor/parent component to a custom component rather than directly to the "styled" component. I found a discussion about it in the styled-components GitHub issues. Following the thread from there there is discussion of utilising transient props and their ultimate inclusion in styled-components v5.1.
This however didn't seem to solve the problem completely in my case.
The problem appears to be due to the component in question returning an HTML div element and so it is extended correctly (by React.HTMLAttributes) to include color: string | undefined as a DOM attribute for that element. This is of course not compatible with ColorProps hence the error. Styled-components filters out a whitelist that includes color however this won't happen in your custom or HOC.
This can be resolved in a number of ways, but the cleanest seems to be adding as?: React.ElementType to your type definition.
In this case:
import React from 'react'
import styled from 'styled-components'
import { color, ColorProps } from 'styled-system'
interface Props extends ColorProps { as?: React.ElementType }
const StyledDiv = styled('div')<Props>`
${color}
`
const Text = ({ color }: Props) => {
return <StyledDiv color={color} />
}
This way the extension by React.HTMLAttributes is replaced by React.ElementType and so there is no longer a conflict with the color DOM attribute.
This also solves problems with passing SpaceProps.
NOTE:
It appears styled-system has been unceremoniously abandoned. There are a few open issues about what is being used to replace it. My recommendation after a little deliberation is system-ui/theme-ui. It seems to be the closest direct replacement and has a few contributors in common with styled-system.
Instead of using ColorProps, try using color: CSS.ColorProperty (`import * as CSS from 'csstype'); Here is a gist showing how I'm creating some a typed "Box" primitive with typescript/styled-system: https://gist.github.com/chiplay/d10435c0962ec62906319e12790104d1
Good luck!
What I did was to use Typescript cast capabilities and keep styled-system logic intact. e.g.:
const Heading: React.FC<ColorProps> = ({ color, children }) => {
return <HeadingContainer color={(color as any)} {...props}>{children}</HeadingContainer>;
};
Just to add to xuanlopez' answer - not sure what issue the 5.0.0 release specifically resolves - but using $color as the renamed prop rather than textColor designates it as a transient prop in styled components so as a prop it won't appear in the rendered DOM.
Building on Chris' answer, and using the latest docs on on custom props.
// core/constants/theme.ts
// Your globally configured theme file
export const theme = { colors: { primary: ['#0A43D2', '#04122B'] } }
// core/constants/styledSystem.ts
import {
color as ssColor,
ColorProps as SSColorProps,
TextColorProps,
compose,
system,
} from 'styled-system'
// Styled-system patch for the color prop fixing "Types of property 'color' are incompatible"
// when appling props to component that extend ColorProps.
export interface ColorProps extends Omit<SSColorProps, 'color'> {
textColor?: TextColorProps['color']
}
export const color = compose(
ssColor,
system({
// Alias color as textColor
textColor: {
property: 'color',
// This connects the property to your theme, so you can use the syntax shown below E.g "primary.0".
scale: 'colors'
}
})
)
// components/MyStyledComponent.ts
import { color, ColorProps } from 'core/constants/styledSystem.ts'
interface MyStyledComponentProps extends ColorProps {}
export const MyStyledComponent = styled.div<MyStyledComponentProps>`
${color}
`
// components/MyComponent.ts
export const MyComponent = () => <MyStyledComponent textColor="primary.0">...
EDIT: updating to styled-components ^5.0.0 fixes this
https://github.com/styled-components/styled-components/blob/master/CHANGELOG.md#v500---2020-01-13