styled-components / xstyled

A utility-first CSS-in-JS framework built for React. šŸ’…šŸ‘©ā€šŸŽ¤āš”ļø
https://xstyled.dev
MIT License
2.28k stars 105 forks source link

Passing TypeScript props to `<x.svg>` errors / correct way to supply props? #255

Closed SpencerDuball closed 3 years ago

SpencerDuball commented 3 years ago

šŸ’¬ Questions and Help

I am trying to pass props to the <x.svg> element below, but am getting errors that I am not having when passing props to other elements such as <x.div> for instance. I looked over the XStyled documentation & source code, styled-components documentation & source code, and TypeScript guide but can't seem to figure this out.

I would like to pass ALL props that <x.svg> can accept ideally, but run into errors extending most of the props. I have tried many different combinations of interfaces and types to pass but keep running into this error.

import { x, SvgProps } from "@xstyled/styled-components";

interface JFEIconProps extends SvgProps {}

export const JFEIcon = (props: JFEIconProps) => (
  <x.svg fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
    <circle cx="194.667" cy="134.333" r="5.867" fill="#E1D29D" />
  </x.svg>
);

This will give me this error:

(JSX attribute) fill?: (string & FillProp<Record<string | number, unknown> & DefaultTheme>) | undefined
No overload matches this call.
  Overload 1 of 2, '(props: Omit<Omit<Pick<Omit<SVGProps<SVGSVGElement>, "color">, "string" | "type" | "className" | "height" | "id" | "lang" | "max" | "media" | "method" | ... 459 more ... | "key"> & { ...; } & SystemProps<...>, "color"> & Partial<...>, "theme"> & { ...; } & { ...; }): ReactElement<...>', gave the following error.
    Type 'FillProp<Theme>' is not assignable to type '(string & FillProp<Record<string | number, unknown> & DefaultTheme>) | undefined'.
      Type 'number & {}' is not assignable to type '(string & FillProp<Record<string | number, unknown> & DefaultTheme>) | undefined'.
        Type 'number & {}' is not assignable to type 'string & BreakpointsProps<"-moz-initial" | "inherit" | "initial" | "revert" | "unset" | "none" | (string & {}) | "aliceblue" | "antiquewhite" | "aqua" | "aquamarine" | "azure" | "beige" | "bisque" | ... 174 more ... | (number & {}), Record<...> & DefaultTheme>'.
          Type 'number & {}' is not assignable to type 'string'.
  Overload 2 of 2, '(props: StyledComponentPropsWithAs<(props: Omit<SVGProps<SVGSVGElement>, "color">) => ReactElement<any, "svg">, DefaultTheme, SystemProps<Record<string | number, unknown> & DefaultTheme>, "color", (props: Omit<...>) => ReactElement<...>>): ReactElement<...>', gave the following error.
    Type 'FillProp<Theme>' is not assignable to type '(string & FillProp<Record<string | number, unknown> & DefaultTheme>) | undefined'.ts(2769)
index.d.ts(2440, 9): The expected type comes from property 'fill' which is declared here on type 'IntrinsicAttributes & Omit<Omit<Pick<Omit<SVGProps<SVGSVGElement>, "color">, "string" | "type" | "className" | "height" | ... 464 more ... | "key"> & { ...; } & SystemProps<...>, "color"> & Partial<...>, "theme"> & { ...; } & { ...; }'
index.d.ts(2440, 9): The expected type comes from property 'fill' which is declared here on type 'IntrinsicAttributes & Omit<Omit<Pick<Omit<SVGProps<SVGSVGElement>, "color">, "string" | "type" | "className" | "height" | ... 464 more ... | "key"> & { ...; } & SystemProps<...>, "color"> & Partial<...>, "theme"> & { ...; } & { ...; }'

Even with other props such as LayoutProps for example, I will still have a mismatch on types. Any help would be greatly appreciated to figure this out. I would be happy to submit a pull request to add the solution to the TypeScript documentation on the website too in order to make this easier for future users.

SpencerDuball commented 3 years ago

Ok I have found a solution for this issue, it looks like the type conflict is because the original props to svg from TypeScript are also still present on <x.svg> instead of only the props from xstyled.

The workaround is to omit the original props from svg that are supplied from TypeScript, example below:

import { SVGProps } from "react";
import { x, SvgProps } from "@xstyled/styled-components";

type SvgPropsKeys = keyof SVGProps<SVGElement>;
interface JFEIconProps extends Omit<SvgProps, SvgPropsKeys> {}

export const JFEIcon = (props: JFEIconProps) => (
  <x.svg fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
    <circle cx="194.667" cy="134.333" r="5.867" fill="#E1D29D" />
  </x.svg>
);

Note that this solves the issue for all TypeScript utilities applied to the x.svg element, for example extending SystemProps like the example below would pass all of the props that x.svg could use.

import { SVGProps } from "react";
import { x, SystemProps } from "@xstyled/styled-components";

type SvgPropKeys = keyof SVGProps<SVGElement>;
interface JFEIconProps extends Omit<SystemProps, SvgPropKeys> {}

export const JFEIcon = (props: JFEIconProps) => (
  <x.svg fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
    <circle cx="194.667" cy="134.333" r="5.867" fill="#E1D29D" />
  </x.svg>
);

I think this is probably a bug now that I understand the issue better, the solution provided in this comment will work as a workaround but I will investigate for a more permanent fix so that this is not an issue out-of-box.

gregberge commented 3 years ago

It is normal that <x.svg /> accepts SVG props and xstyled props.

SpencerDuball commented 3 years ago

Just want to leave this comment here for anyone that might run into this issue, this is a drop in solution that you can use to resolve these type conflicts.

I think that <x.svg> should accept SVG props, but I think it might be better to remove the intersecting types and prefer the type definitions from <x.svg> only when using that component instead of also the type definitions from React.SVGProps<SVGSVGElement>.

When styling on HTML elements such as <x.p> we are adding props such as backgroundColor that are then applied via the class property, but with <x.svg> we are not adding props, but replacing props such as fill. This is an issue because fill has both the type definitions of React.SVGProps<SVGSVGElement> and SystemProps, and in this case there is a type conflict when trying to set attributes such as fill to a string (see original issue for details on this error).

An example of removing the intersecting types is provided below in the workaround and the component is working as expected.

TLDR; The Workaround

Create a new file called XSvg.tsx (or whatever name is preferred), and use this instead of x.svg:

// XSvg.tsx
import { x, SystemProps } from "@xstyled/emotion";
import React from "react";

// fix for <x.svg> type conflicts
type XSvgKeys = keyof Omit<SystemProps, "children">;
type UniqueIntrinsicSvgProps = Omit<React.SVGProps<SVGSVGElement>, XSvgKeys>;
export interface XSvgProps extends UniqueIntrinsicSvgProps, SystemProps {}
export const XSvg: (props: XSvgProps) => JSX.Element = x.svg as any;

And an example use case:

// Abacus.tsx
import React from "react";
import { SystemProps } from "@xstyled/emotion";
import { XSvg } from "utility";

export interface AbacusPropsI extends SystemProps {}

export const Abacus = (props: AbacusPropsI) => (
  <XSvg
    xmlns="http://www.w3.org/2000/svg"
    viewBox="0 0 24 24"
    fill="black"
    {...props}
  >
    <path d="M21,2a1,1,0,0,0-1,1V6H16V5a1,1,0,0,0-2,0V6H12V5a1,1,0,0,0-2,0V6H8V5A1,1,0,0,0,6,5V6H4V3A1,1,0,0,0,2,3V19a3,3,0,0,0,3,3H19a3,3,0,0,0,3-3V3A1,1,0,0,0,21,2ZM20,19a1,1,0,0,1-1,1H5a1,1,0,0,1-1-1V16H6v1a1,1,0,0,0,2,0V16h2v1a1,1,0,0,0,2,0V16h4v1a1,1,0,0,0,2,0V16h2Zm0-5H18V13a1,1,0,0,0-2,0v1H12V13a1,1,0,0,0-2,0v1H8V13a1,1,0,0,0-2,0v1H4V8H6V9A1,1,0,0,0,8,9V8h2V9a1,1,0,0,0,2,0V8h2V9a1,1,0,0,0,2,0V8h4Z" />
  </XSvg>
);