Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The type of styled-component does not match for "strict: true" #13921

Open
konojunya opened this issue Dec 17, 2018 · 18 comments
Open

The type of styled-component does not match for "strict: true" #13921

konojunya opened this issue Dec 17, 2018 · 18 comments

Comments

@konojunya
Copy link

konojunya commented Dec 17, 2018

When we use styled-component in TypeScript to inherit the Material-UI component, props type does not match.
How do I overwrite a style with styled-component?

There is a sample on how to inherit in the case of styled-component in the following link, but this sample will generate an error if it is set to strict: true in tsconfig setting.

https://material-ui.com/guides/interoperability/#styled-components

import React from 'react';
import styled from 'styled-components';
import Button from '@material-ui/core/Button';

const StyledButton = styled(Button)`
  background: linear-gradient(45deg, #fe6b8b 30%, #ff8e53 90%);
  border-radius: 3px;
  border: 0;
  color: white;
  height: 48px;
  padding: 0 30px;
  box-shadow: 0 3px 5px 2px rgba(255, 105, 135, .3);
`;

function StyledComponentsButton() {
  return (
    <div>
      <Button>
        Material-UI
      </Button>
      <StyledButton>
        Styled Components
      </StyledButton>
    </div>
  );
}

export default StyledComponentsButton;

The way I tried is to cast the component with React.SFC and then pass the props type of the material-ui component to that generics.

import TextField, { TextFieldProps } from "@material-ui/core/TextField";

export const StyledTextField = styled(TextField as React.SFC<TextFieldProps>)`
  // some style
`;

in this case

Type '{ disabled: true; label: string; defaultValue: string; variant: "outlined"; }' is missing the following properties from type 'Pick<Pick<TextFieldProps.....

The above error will be displayed :(

It was able to solve by the following method.

interface TextFieldProps {
  disabled: boolean;
  label: string;
  defaultValue: string;
  variant: string;
}

export const StyledTextField = styled(TextField as React.SFC<TextFieldProps>)`
  // some style
`;

However, I do not think this is a nice implementation. Required Props
It is because you have to re-implement the interface every time it changes.


@material-ui/core version: 3.6.2
@material-ui/icons version: 3.0.1
styled-components version: 4.1.2
typescript version: 3.2.2

macOS: 10.13.6

@ghost
Copy link

ghost commented Dec 17, 2018

There's some documentation on the TypeScript Guide page related to using type widening with createStyles to overcome some of these issues: https://material-ui.com/guides/typescript/

@danprat92
Copy link

Same issue here.

In case it's helpful file with issue is:

import { TextField } from '@material-ui/core';
import styled from '../../theme';

export const CustomSelect = styled(TextField)`
  width: 100%;
  max-width: 300px;
`;

And tsconfig is:

{
  "compilerOptions": {
    "allowJs": true,
    "baseUrl": ".",
    "forceConsistentCasingInFileNames": true,
    "jsx": "react",
    "lib": ["es6", "dom"],
    "module": "esnext",
    "moduleResolution": "node",
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noUnusedLocals": true,
    "outDir": "build",
    "paths": {
      "src/*": ["src/*"],
      "test/*": ["test/*"]
    },
    "rootDir": "src",
    "sourceMap": true,
    "strictNullChecks": true,
    "suppressImplicitAnyIndexErrors": true,
    "target": "es5",
    "typeRoots": ["src/typings", "node_modules/@types"],
  },
  "include": [
    "src",
    "test"
  ]
}

@konojunya
Copy link
Author

konojunya commented Dec 28, 2018

These problems can also be overridden by style unless it is strict: true. If you check strictly against type, you will get an error.

This happens because interface is defined as follows, but you can also see the idea of the author.

export interface BaseTextFieldProps
  extends StandardProps<FormControlProps, TextFieldClassKey, 'onChange' | 'defaultValue'> {
  autoComplete?: string;
  autoFocus?: boolean;
  children?: React.ReactNode;
  defaultValue?: string | number;
  disabled?: boolean;
  error?: boolean;
  FormHelperTextProps?: Partial<FormHelperTextProps>;
  fullWidth?: boolean;
  helperText?: React.ReactNode;
  id?: string;
  InputLabelProps?: Partial<InputLabelProps>;
  inputRef?: React.Ref<any> | React.RefObject<any>;
  label?: React.ReactNode;
  margin?: PropTypes.Margin;
  multiline?: boolean;
  name?: string;
  onChange?: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>;
  placeholder?: string;
  required?: boolean;
  rows?: string | number;
  rowsMax?: string | number;
  select?: boolean;
  SelectProps?: Partial<SelectProps>;
  type?: string;
  value?: Array<string | number | boolean> | string | number | boolean;
}

export interface StandardTextFieldProps extends BaseTextFieldProps {
  variant?: 'standard';
  InputProps?: Partial<StandardInputProps>;
  inputProps?: StandardInputProps['inputProps'];
}

export interface FilledTextFieldProps extends BaseTextFieldProps {
  variant: 'filled';
  InputProps?: Partial<FilledInputProps>;
  inputProps?: FilledInputProps['inputProps'];
}

export interface OutlinedTextFieldProps extends BaseTextFieldProps {
  variant: 'outlined';
  InputProps?: Partial<OutlinedInputProps>;
  inputProps?: OutlinedInputProps['inputProps'];
}

export type TextFieldProps = StandardTextFieldProps | FilledTextFieldProps | OutlinedTextFieldProps;

So I solved this problem as follows.
Although this is not the best policy, I feel that it is more a matter of operation as a team rather than a material-ui problem any more.
Include the component in a specific directory that extends material - ui.
Taking TextField as an example

src/components/extensions/TextFiled

import * as React from "react";
import {
  default as MUITextFiled,
  StandardTextFieldProps,
  FilledTextFieldProps,
  OutlinedTextFieldProps
} from "@material-ui/core/TextField";

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

export const StandardTextField: React.ComponentType<Omit<StandardTextFieldProps, "variant">> = props => (
  <MUITextFiled {...props} variant="standard" />
);
export const FilledTextField: React.ComponentType<Omit<FilledTextFieldProps, "variant">> = props => (
  <MUITextFiled {...props} variant="filled" />
);
export const OutlinedTextField: React.ComponentType<Omit<OutlinedTextFieldProps, "variant">> = props => (
  <MUITextFiled {...props} variant="outlined" />
);

From another program, we do not import import Material - UI 's TextField so we will import from this extensions . This eliminates TypeScript type problems.

@eps1lon
Copy link
Member

eps1lon commented Dec 28, 2018

The base problem is with type unions i.e. Foo | Bar and Omit. I don't think we can do much here beyond using any cast:

import TextField, { TextFieldProps } from '@material-ui/core/TextField';

const StyledTextField: React.ComponentType<TextFieldProps> = styled(TextField)({}) as any;

Not sure if this defeats any type checking in styled though.

@danprat92

This comment has been minimized.

@Mario-Eis
Copy link

Mario-Eis commented Jan 8, 2019

I think this should be related to DefinitelyTyped/DefinitelyTyped#29832 and possibly DefinitelyTyped/DefinitelyTyped#30942

I also suggest, that this issue is not only causing problems for strict: true. In my typescript project it leaves material-ui completely incompatible to styled-components (after updating @types/styled-components to >=4.1.0). And all I use is strictNullChecks, noImplicitAny and noImplicitThis.

Every single component is affected.
e.g.

declare const Typography: React.ComponentType<TypographyProps>;
....
type ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P>;

Please correct me here if this is a different problem. I will open a new issue then.

@eps1lon
Copy link
Member

eps1lon commented Apr 17, 2019

I encourage you to use

const StyledButton = styled(Button)`` as typeof Button;

This will remove any type information for props added by styled-components. However, most of these shouldn't be necessary anymore. Please let me know if there are any props from styled-components that are essential in your opinion.

There are both issues with the styled-component types (union props are lost: changing this caused a 20x perf regression in ts 3.4) and TypeScript design limitations (argument inference only considers one function overload).

@radiosterne
Copy link

@eps1lon by changing to not losing union props you are meaning switching from using Pick to using Pick distributed over union with extends any conditional clause? If so, can you provide an example of perf regression? I've monkey-patched my local styled-components definitions and as of TS 3.5 unable to notice any regression whatsoever — maybe TS team updated smth related in TS 3.5?

@eps1lon
Copy link
Member

eps1lon commented Jun 17, 2019

@radiosterne Sounded like it. The regression was dicovered in 3.4: microsoft/TypeScript#30663

@radiosterne
Copy link

@eps1lon that's great; I take it we are now able to substitute Pick for a better version, or do you mind? I'll make a pull request to DefinitelyTyped, if you're ok with it.

@oliviertassinari
Copy link
Member

@radiosterne Have you done some progress on the topic?

@nstolmaker
Copy link

nstolmaker commented Mar 4, 2021

I'm not an expert here, but if anyone else is using Material-UI with Styled Components, and has tried to style a Typography element and is getting the error described here about type 'component' does not exist on type...'
Here's a fix:

Put this somewhere (I have mine in a utils/types.tsx file, hence the export).

interface OverridableComponent<M extends OverridableTypeMap> {
  <C extends React.ElementType>(props: { component: C, theme?: Theme } & OverrideProps<M, C>): JSX.Element;
  (props: { theme?: Theme } & DefaultComponentProps<M>): JSX.Element;
}
export interface TypographyPropsFix extends OverridableComponent<TypographyTypeMap> { }

Then import it into the file where you're styling Typography elements:
import { TypographyPropsFix } from 'utils/types'

Then you just have to cast StyledComponents definitions that use Typography to the fixed type, e.g.:

const StyledBodyParagraphs2 = styled(Typography)`
  margin: 0 0 1rem 0;
  text-Align: justify;
  ${props => props.theme.breakpoints.up("md")} {
    margin: 0 0rem 1rem 0;
    padding: 5px;
  }
` as TypographyPropsFix

and Voila! Typescript let's you get on with your life!

@eps1lon
Copy link
Member

eps1lon commented Mar 5, 2021

@nstolmaker This seems like a more elaborate way to do #13921 (comment) i.e. styled(Typography) as typeof Typography is the simpler approach. What kind of issue is your suggestion fixing that isn't already fixed by #13921 (comment)?

@nstolmaker
Copy link

@eps1lon Good question. The way described works fine for Button components, but the Typography component won't work that way. It gives the same error about Component not being defined.

@Tiberriver256
Copy link

@eps1lon - Great solution but doesn't seem to work when extending the props with a few extra properties that are filtered out using shouldForwardProp.

@emotion/styled forwarding props in Typescript

For example, in the Button component of this library. ownerState is not a prop on ButtonBase but I need to be able to do this:

<ButtonRoot ownerState={state} />

I would normally write something like this:

const ButtonRoot = styled(ButtonBase, { shouldForwardProp: prop => prop !== 'ownerState' })<{ownerState: OwnerStateProps>({ownerstate}) => ({
   // user ownerState in styles
})

<ButtonRoot ownerState={state} />

If I cast it as a typeof ButtonBase, I lose my ownerState prop.

@jdrucey
Copy link

jdrucey commented May 17, 2022

@Tiberriver256 has highlighted our exact problem with the previously mentioned solutions of simply using typeof

Did you ever figure out a workaround @Tiberriver256 ?

@jdrucey
Copy link

jdrucey commented May 17, 2022

Just in case it helps anyone else, I've figured this out for our two edge cases where typeof is not sufficient:

styled with a native HTML Element

use as React.FC<{ sx?: SxProps }>

import React from 'react'
import { SxProps } from '@mui/system'
import { styled } from '@mui/material/styles'

const Badge = styled('span')(({ theme }) => ({
  border: '1px solid ' + theme.palette.primary.main,
  borderRadius: '.15rem',
  color: theme.palette.primary.main,
  display: 'inline-block',
  fontSize: '.55rem',
  fontWeight: 600,
  lineHeight: '.8rem',
  marginRight: theme.spacing(0.5),
  padding: '0 .3em',
  textAlign: 'center',
  textTransform: 'uppercase',
  verticalAlign: 'middle',
  whiteSpace: 'nowrap'
})) as React.FC<{ sx?: SxProps }>

export default Badge

styled with custom props

Use as React.FC<YourCustomProps>

import React from 'react'
import { styled } from '@mui/material/styles'
import TableCell, { TableCellProps } from '@mui/material/TableCell'

interface IEnhancedTableCell extends TableCellProps {
  disabled?: boolean
}

const EnhancedTableCell = styled((props: IEnhancedTableCell) => (
  <TableCell {...props} />
))(({ theme }) => ({
  root: {
    '& a': {
      color: (props: IEnhancedTableCell) =>
        props.disabled
          ? theme.palette.text.primary
          : theme.palette.primary.main,
      textDecoration: 'none'
    },
    opacity: (props: IEnhancedTableCell) => (props.disabled ? 0.3 : 1)
  }
})) as React.FC<IEnhancedTableCell>

export default EnhancedTableCell

Obviously, these are centred around functional components, but for preserving typings and ensuring children are allowed, these work for our use case.

@WillSquire
Copy link
Contributor

@jdrucey is right, it's still a problem with custom props. To be honest, I end up writing less code and find it easier to maintain if I just do:

const Title = styled(Typography)<{
  // fix for `component` typing
  // see: https://github.com/mui/material-ui/issues/13921
  component: React.ElementType
  withImage: boolean
}>(({ theme, withImage }) => ({
  // etc...
}))

MUI uses React.ElementType as the base type for the generic in OverridableComponent.

I feel like this shouldn't be an issue, though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants