Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Styled Components, required props defined as defaultProps show as missing

Required props that are defined as defaultProps show as missing when using the component.

// Button.ts
interface ButtonProps {
  size: 'small' | 'medium' | 'large';
  inverted: boolean;
  raised: boolean;
}

const Button = styled('button')<ButtonProps>`...`

Button.defaultProps = {
  size: 'medium',
  inverted: false,
  raised: false,
}

export default Button
// Hello.tsx
import Button from './Button'

const Hello = () => <Button>Hello</Button>

Button is highlighted in red and displays this error message:

Type '{}' is missing the following properties from type 
'Pick<Pick<Pick<DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement>, "form" | "style" | "title" | "key" | "autoFocus" |
"disabled" | "formAction" | ... 255 more ... | "onTransitionEndCapture"> &
{ ...; } & ButtonProps, "form" | ... 329 more ... | "raised"> & 
Partial<...>, "form" | ... 329 ...': size, inverted, and 1 more.

Versions:

"@types/styled-components": "^4.1.9",
"@types/react": "^16.8.5",
"@types/react-dom": "^16.8.2",

"typescript": "^3.3.3333",
"styled-components": "^4.1.3"
"react": "^16.8.3",
"react-dom": "^16.8.3",
like image 366
Matt Thomas Avatar asked Jan 26 '23 16:01

Matt Thomas


2 Answers

Edit While I'm personally ok with this behavior, it's not consistent with behavior from a normal React component (typescript supports defaultProps).

I look around a bit, and it seems that this was fixed in @types/styled-components, but then removed due to it introducing another bug.

Note from the type maintainer:

However, TS doesn't set type of newly set defaultProps (aka expando), so don't expect it would allow to skip required props. In order to modify the type, in the tests I added a simple helper function that sets defaultProps and returns modified type. This is meant to be an example only.

Here's that example:

// example of a simple helper that sets defaultProps and update the type
type WithDefaultProps<C, D> = C & { defaultProps: D };

function withDefaultProps<C, D>(component: C, defaultProps: D): WithDefaultProps<C, D> {
    (component as WithDefaultProps<C, D>).defaultProps = defaultProps;
    return component as WithDefaultProps<C, D>;
}

This helper does fix the problem (and is similar to @TitianCernicova-Dragomir's approach so you should consider marking his answer as correct if you accept this as a correct workaround).

/* setup */
interface Props {
    requiredProp: number;
}

const defaultProps: Props = {
    requiredProp: 1,
};

const Component = styled.div<Props>``;
const ComponentWithDefault = withDefaultProps(Component, defaultProps);

export { Component, ComponentWithDefault };

/* usage */
import { Component, ComponentWithDefault } from './component.tsx';

const a = <Component /> // Property requiredProp is missing...
const b = <ComponentWithDefault /> // no error

Requirements:

"@types/styled-components": "^4.1.12",
"typescript": "^3.2.4",

defaultProps with styled-components did worked in the past — if you're interested in keeping that behavior, I think reverting to older version of @types/styled-components might help; I think it's 4.1.8 but I'm not sure.

Original answer


Perhaps not the answer you're looking for, but I find this behavior desireable. If you have already defined certain properties in defaultProps, wouldn't that make those properties optional?

I'd make defaulted props optional:

interface ButtonProps {
  size?: 'small' | 'medium' | 'large';
  inverted?: boolean;
  raised?: boolean;
}

Or keep it intact, but wrap in Partial<> since you're already provide default for all of them

const Button = styled('button')<Partial<ButtonProps>>`...`

Another things I usually do is to define defaultProps first in a variable, so I can properly define their interface:

interface Props { ... }

const defaultProps: Props = { ... }

const Component = styled.div<Props>`...`
Component.defaultProps = defaultProps
like image 91
Derek Nguyen Avatar answered Jan 31 '23 23:01

Derek Nguyen


The problem is that assigning defaultProps will not change it's type which will remain the default of Partial<StyledComponentProps<"button", any, ButtonProps, never>>. You can explicitly type the const but typing a lot of types is never fun. A simpler approach is to use another HOC (everything can be solved with another HOC 😃) that mutates the type of defaultProps to contain the default props you actually set on the component:

// Button.ts
interface ButtonProps {
    size: 'small' | 'medium' | 'large';
    inverted: boolean;
    raised: boolean;
}

function withDefault<T extends { defaultProps?: Partial<TDefaults> }, TDefaults>(o: T, defaultProps: TDefaults): T & { defaultProps: TDefaults } {
    o.defaultProps = defaultProps;
    return o as any;
}

const Button = withDefault(styled('button') <ButtonProps>`...`, {
    size: 'medium',
    inverted: false,
    raised: false,
})

export default Button

const Hello = () => <Button>Hello</Button> // OK now
like image 40
Titian Cernicova-Dragomir Avatar answered Jan 31 '23 21:01

Titian Cernicova-Dragomir