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)