diff --git a/docs/API.md b/docs/API.md index f1429bbde..218d04151 100644 --- a/docs/API.md +++ b/docs/API.md @@ -66,7 +66,9 @@ Unlike the [`classnames`](https://www.npmjs.com/package/classnames) library, thi ### `styled` -Helper to build React components. It allows you to write your components in a similar syntax as [`styled-components`](https://www.styled-components.com/): +Helper to build React components. It allows you to write your components in a similar syntax as [`styled-components`](https://www.styled-components.com/). + +[Typescript documentation and examples](./TYPESCRIPT.md) for the `styled` API is also available. The syntax is similar to the `css` tag. Additionally, you can use function interpolations that receive the component's props: diff --git a/docs/TYPESCRIPT.md b/docs/TYPESCRIPT.md new file mode 100644 index 000000000..6a3ff27b4 --- /dev/null +++ b/docs/TYPESCRIPT.md @@ -0,0 +1,173 @@ +# Linaria TypeScript API + +## Styling HTML Elements + +The `styled` tag infers types automatically for HTML elements. To add custom props, pass them as a type parameter: + +```typescript +import { styled } from '@linaria/react'; + +const Title = styled.div<{ background: string }>` + background: ${(props) => props.background}; +`; + +Hello; +``` + +Extract prop interfaces when they'll be reused: + +```typescript +interface ButtonProps { + color: string; + size: 'small' | 'large'; +} + +const Button = styled.button` + color: ${(props) => props.color}; + font-size: ${(props) => props.size === 'small' ? '12px' : '16px'}; +`; +``` + +## Wrapping Styled Components + +You can extend an existing styled component with additional props. Both the original and new props are merged and available in template expressions: + +```typescript +const Button = styled.button<{ outline: boolean }>` + background-color: ${(props) => props.outline ? 'transparent' : 'royalblue'}; + border: 2px solid royalblue; +`; + +const DangerButton = styled(Button)<{ label: string }>` + background-color: ${(props) => props.outline ? 'transparent' : 'crimson'}; + border-color: crimson; + color: ${(props) => props.label}; +`; + +// Both outline (from Button) and label (from DangerButton) are required +; +``` + +## Wrapping Custom React Components + +The component being wrapped must accept `className` as an optional prop, otherwise TypeScript will error: + +```typescript +// ✅ Component accepts className +const Card: React.FC<{ className?: string; title: string }> = ({ + className, + title, +}) =>
{title}
; + +const StyledCard = styled(Card)` + border: 1px solid gray; +`; + +; +``` + +If you're passing dynamic CSS values that resolve to inline styles, the component also needs a `style` prop: + +```typescript +const Card: React.FC<{ + className?: string; + style?: React.CSSProperties; + title: string; +}> = ({ className, style, title }) => ( +
{title}
+); + +const StyledCard = styled(Card)<{ borderRadius: number }>` + border: 1px solid gray; + border-radius: ${(props) => props.borderRadius}px; +`; + +; +``` + +## Union Types and Discriminated Unions + +Discriminated unions in component props work correctly: + +```typescript +type GridProps = + | { container?: false } + | { container: true; spacing: number }; + +const Grid: React.FC = (props) => ( +
+); + +const StyledGrid = styled(Grid)` + display: grid; +`; + +; // ✅ +; // ✅ +// @ts-expect-error +; // ✅ correctly rejected +``` + +## Higher-Order Functions + +When wrapping styled components in a higher-order function, constrain the generic to ensure the component accepts `className`: + +```typescript +interface BaseProps { + className?: string; + style?: React.CSSProperties; +} + +const withFlexColumn = (Cmp: React.FC) => + styled(Cmp)` + display: flex; + flex-direction: column; + `; + +interface CardProps extends BaseProps { + title: string; +} + +const Card: React.FC = (props) =>
; +const FlexCard = withFlexColumn(Card); + +; // title prop is preserved and required +``` + +You can compose multiple wrappers. Each wrapper injects its own props by including them in the generic constraint, then casting the component argument so inference doesn't fight the `styled` overloads: + +```typescript +const withSpacing = ( + Component: React.FC +) => + styled(Component as React.FC)` + gap: ${(props) => (props as any).gap}px; + `; + +const withWidth = ( + Component: React.FC +) => + styled(Component as React.FC)` + width: ${(props) => (props as any).fluid ? '100%' : 'auto'}; + `; + +const Base: React.FC<{ + label: string; + className?: string; + style?: React.CSSProperties; + gap: number; + fluid: boolean; +}> = (props) =>
; + +const Enhanced = withSpacing(withWidth(Base)); + +; // all props available and type-safe +``` + +Whenever a wrapper uses dynamic values in template expressions, Linaria requires the component to have both `className` and `style` props. The injected props (`gap`, `fluid`) also need to live in the generic constraint rather than being intersected after the fact with `styled(Component)` — that form hits a `styled` overload that expects a plain object, resolving the component to `never`. + +## See Also + +- [General API docs](./API.md) +- [Type definition tests](https://github.com/callstack/linaria/blob/master/packages/react/__dtslint__/styled.ts) +- [Conditional Types in the TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html)