Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to add properties to styled component with Typescript?

I have components like this

import React from 'react';

import styled from '../../styled-components';

const StyledInput = styled.input`
    display: block;
    padding: 5px 10px;
    width: 50%;
    border: none;
    border-radius: 10px;
    outline: none;
    background: ${(props) => props.theme.color.background};
    font-family: 'Roboto', sans-serif;
`
;

export const Input = () => {
    return <StyledInput placeholder="All notes"></StyledInput>
}

I want to place them as property on "index" component like this

import styled from 'styled-components';

import { Input } from './Input';

const NoteTags:any = styled.div`
    display: flex;
    justify-content: space-between;
    align-items: center;
    width: 20%;
    height: 5%;
    border-bottom: 2px solid ${(props) => props.theme.color.background}; `;

NoteTags.Input = Input;

export default NoteTags;

So then I can use them like this

import React from 'react';

import NoteTags from '../../blocks/note-tags';

const ComponentNoteTags = () => {
    return (
        <React.Fragment>
            <NoteTags.Input></NoteTags.Input>
        </React.Fragment>
    )
}

export default ComponentNoteTags;

The problem with this is that property Input doesn't exist on NoteTags so typescript gives me an error. I can solve it by setting type any to NoteTags but I'm not sure that it is the best way.

Is there a better way to fix it?

like image 345
Person Avatar asked Feb 21 '19 17:02

Person


People also ask

How are properties passed to styled-components?

To pass props to React components created with styled-components, we can interpolate functions into the string that creates the component. We create the ArrowStyled component with the styled. div tag. Then string that we pass into the tag has the rotate value set from a prop.


2 Answers

Update

I found it much better to use a type helper to add extra properties into a component's variable:

// type-helper.ts
export function withProperties<A, B>(component: A, properties: B): A & B {
  Object.keys(properties).forEach(key => {
    component[key] = properties[key]
  });
  return component as A & B;
}

Note: @Allan points out in the comment, type assertion might be necessary like so:

(component as any)[key] = (properties as any)[key]

to avoid typescript complaining about Element implicitly has an 'any' type because type '{}' has no index signature.

We're just passing each property in properties to component and give it a joined type of both. Then we can 'glue' sub components into a main component as follow:

// component/index.ts
import MainComponent from './MainComponent';
import SubComponent1 from './SubComponent1';
import SubComponent2 from './SubComponent2';
import { withProperties } from '../type-helper.ts';

export withProperties(MainComponent, { SubComponent1, SubComponent2 })

and use it as usual

import MainComponent from './Component';

export default () => (
  <MainComponent>
    <SubComponent1 />
    <SubComponent2 />
  </MainComponent>
)

This approach works with normal React component as well & not just styled components one, so I think it's better.

Here's a codesandbox demo with both approaches I've shared, one of each use your code.


Previous answer

Assuming you're using the latest packages:

"styled-components": "^4.1.3",
"@types/styled-components": "^4.1.10",

You can create an interection type that include both the type of the parent & child components:

import styled, { StyledComponent } from 'styled-components'
import ChildComponent from '...'

type Component = StyledComponent<'div', any> & {
    ChildComponent: /* your component type */
}

interface IProps {
  ...
}

// create styled component per usual
const MyComponent = styled.div<IProps>`
  ...
` as Component

MyComponent.ChildComponent = ChildComponent;

Here's the StyledComponent definition:

export type StyledComponent<
    C extends keyof JSX.IntrinsicElements | React.ComponentType<any>,
    T extends object,       // theme interface
    O extends object = {},  // props interface
    A extends keyof any = never
> = string & StyledComponentBase<C, T, O, A>;

You can use it as StyledComponent<C, T, O, A> or StyledComponent<C, T>

like image 103
Derek Nguyen Avatar answered Oct 19 '22 11:10

Derek Nguyen


I've managed to get it working by defining the call signatures of the styled component and the additional properties you want to add. Typescript will complain that Item doesn't exist on List, so I had to use Object Assign to create the styled component and assign the property simultaneously.

import styled from 'styled-components'
import { PropsWithChildren, ReactElement } from 'react'

interface ListOverload {
    ({ children }: PropsWithChildren<{}>): ReactElement
    Item({ children }: PropsWithChildren<{}>): ReactElement
}

export const Item = styled.li(
    ({ theme }) => css`
        padding: ${theme.space.md};
        border-bottom: 1px solid ${theme.colors.greyLight};
    `
)

export const List: ListOverload = Object.assign(
    styled.ul`
        list-style: none;
        padding: 0;
        margin: 0;
    `,
    { Item }
)

Then you can use like this in your JSX:

<List>
  <List.Item>Todo 1</List.Item>
  <List.Item>Todo 2</List.Item>
  <List.Item>Todo 3</List.Item>
</List>

like image 1
laij84 Avatar answered Oct 19 '22 11:10

laij84