Emotion与TypeScript #
基本概念 #
Emotion 提供了完整的 TypeScript 支持,让你可以在类型安全的环境下编写样式代码。本节介绍如何在 TypeScript 项目中使用 Emotion。
基本配置 #
安装类型 #
bash
npm install @emotion/react @emotion/styled
npm install -D @types/react
tsconfig.json 配置 #
json
{
"compilerOptions": {
"jsxImportSource": "@emotion/react",
"types": ["@emotion/react/types/css-prop"]
}
}
组件类型定义 #
styled 组件类型 #
tsx
import styled from '@emotion/styled'
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger'
size?: 'small' | 'medium' | 'large'
disabled?: boolean
}
export const Button = styled.button<ButtonProps>`
padding: ${props => {
switch (props.size) {
case 'small': return '6px 12px'
case 'large': return '14px 28px'
default: return '10px 20px'
}
}};
background-color: ${props => {
switch (props.variant) {
case 'primary': return '#007bff'
case 'secondary': return '#6c757d'
case 'danger': return '#dc3545'
default: return '#007bff'
}
}};
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`
css prop 类型 #
tsx
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
interface CardProps {
elevated?: boolean
padding?: number
}
export function Card({ elevated, padding = 16 }: CardProps) {
return (
<div
css={css`
padding: ${padding}px;
background: white;
border-radius: 8px;
box-shadow: ${elevated
? '0 4px 12px rgba(0, 0, 0, 0.15)'
: '0 2px 4px rgba(0, 0, 0, 0.1)'};
`}
>
Card content
</div>
)
}
主题类型 #
扩展 Theme 接口 #
创建 emotion.d.ts 文件:
tsx
import '@emotion/react'
declare module '@emotion/react' {
export interface Theme {
colors: {
primary: string
secondary: string
success: string
danger: string
warning: string
info: string
background: string
surface: string
text: string
textMuted: string
border: string
}
fontSizes: {
xs: string
sm: string
md: string
lg: string
xl: string
}
spacing: {
xs: string
sm: string
md: string
lg: string
xl: string
}
borderRadius: {
sm: string
md: string
lg: string
}
shadows: {
sm: string
md: string
lg: string
}
}
}
使用主题类型 #
tsx
import styled from '@emotion/styled'
import { Theme } from '@emotion/react'
export const Button = styled.button<{ variant?: keyof Theme['colors'] }>`
background-color: ${props => props.theme.colors[props.variant || 'primary']};
color: white;
padding: ${props => props.theme.spacing.sm} ${props => props.theme.spacing.md};
border: none;
border-radius: ${props => props.theme.borderRadius.sm};
`
主题文件 #
tsx
import { Theme } from '@emotion/react'
export const lightTheme: Theme = {
colors: {
primary: '#007bff',
secondary: '#6c757d',
success: '#28a745',
danger: '#dc3545',
warning: '#ffc107',
info: '#17a2b8',
background: '#ffffff',
surface: '#f8f9fa',
text: '#333333',
textMuted: '#6c757d',
border: '#dee2e6',
},
fontSizes: {
xs: '12px',
sm: '14px',
md: '16px',
lg: '18px',
xl: '20px',
},
spacing: {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
},
borderRadius: {
sm: '4px',
md: '8px',
lg: '12px',
},
shadows: {
sm: '0 1px 2px rgba(0, 0, 0, 0.05)',
md: '0 4px 6px rgba(0, 0, 0, 0.1)',
lg: '0 10px 15px rgba(0, 0, 0, 0.1)',
},
}
类型工具 #
提取 Props 类型 #
tsx
import styled from '@emotion/styled'
import { ComponentProps } from 'react'
type ButtonBaseProps = ComponentProps<'button'>
interface ButtonProps extends ButtonBaseProps {
variant?: 'primary' | 'secondary'
size?: 'small' | 'medium' | 'large'
}
export const Button = styled.button<ButtonProps>`
padding: ${props => props.size === 'large' ? '14px 28px' : '10px 20px'};
background: ${props => props.variant === 'primary' ? '#007bff' : '#6c757d'};
color: white;
`
样式函数类型 #
tsx
import { css, Interpolation, Theme } from '@emotion/react'
type StyleFunction<P = {}, T = Theme> = (
props: P & { theme: T }
) => Interpolation<Theme>
export const getButtonStyles: StyleFunction<{ variant?: string }> = (props) => css`
background: ${props.variant === 'primary' ? props.theme.colors.primary : '#6c757d'};
color: white;
`
泛型组件 #
泛型样式组件 #
tsx
import styled from '@emotion/styled'
interface ListProps<T> {
items: T[]
renderItem: (item: T) => React.ReactNode
}
export function List<T>({ items, renderItem }: ListProps<T>) {
return (
<Container>
{items.map((item, index) => (
<Item key={index}>{renderItem(item)}</Item>
))}
</Container>
)
}
const Container = styled.ul`
list-style: none;
padding: 0;
margin: 0;
`
const Item = styled.li`
padding: 12px;
border-bottom: 1px solid #eee;
&:last-child {
border-bottom: none;
}
`
泛型样式函数 #
tsx
import { css, Interpolation } from '@emotion/react'
function createResponsiveStyle<T extends string>(
breakpoints: Record<T, string>
) {
return (key: T, styles: string): Interpolation => css`
@media (min-width: ${breakpoints[key]}) {
${styles}
}
`
}
const breakpoints = {
sm: '576px',
md: '768px',
lg: '992px',
} as const
const responsive = createResponsiveStyle(breakpoints)
const styles = css`
${responsive('md', 'font-size: 18px;')}
${responsive('lg', 'font-size: 20px;')}
`
类型守卫 #
Props 验证 #
tsx
import styled from '@emotion/styled'
type ButtonVariant = 'primary' | 'secondary' | 'danger'
interface ButtonProps {
variant?: ButtonVariant
size?: 'small' | 'medium' | 'large'
}
function isValidVariant(variant: string): variant is ButtonVariant {
return ['primary', 'secondary', 'danger'].includes(variant)
}
export const Button = styled.button<ButtonProps>`
background-color: ${props => {
const variant = props.variant || 'primary'
return isValidVariant(variant)
? props.theme.colors[variant]
: props.theme.colors.primary
}};
`
条件样式类型 #
tsx
import { css, Interpolation } from '@emotion/react'
type ConditionalStyles<P> = {
[K in keyof P]?: P[K] extends boolean ? Interpolation : never
}
function applyConditionalStyles<P extends object>(
props: P,
styles: ConditionalStyles<P>
): Interpolation[] {
return Object.entries(styles)
.filter(([key]) => props[key as keyof P])
.map(([, style]) => style)
}
interface ButtonProps {
primary?: boolean
disabled?: boolean
}
const buttonStyles: ConditionalStyles<ButtonProps> = {
primary: css`background: #007bff; color: white;`,
disabled: css`opacity: 0.5; cursor: not-allowed;`,
}
类型安全的事件处理 #
tsx
import styled from '@emotion/styled'
import { MouseEvent, ChangeEvent } from 'react'
interface InputProps {
value: string
onChange: (e: ChangeEvent<HTMLInputElement>) => void
onClick?: (e: MouseEvent<HTMLInputElement>) => void
}
const StyledInput = styled.input`
padding: 12px;
border: 1px solid #ddd;
border-radius: 4px;
&:focus {
border-color: #007bff;
}
`
export function Input({ value, onChange, onClick }: InputProps) {
return (
<StyledInput
value={value}
onChange={onChange}
onClick={onClick}
/>
)
}
最佳实践 #
1. 导出类型 #
tsx
export type { ButtonProps } from './Button.types'
export { Button } from './Button'
2. 使用 as const #
tsx
export const VARIANTS = ['primary', 'secondary', 'danger'] as const
export type Variant = typeof VARIANTS[number]
3. 严格类型检查 #
tsx
interface StrictButtonProps {
variant: 'primary' | 'secondary'
size: 'small' | 'medium' | 'large'
disabled?: boolean
onClick?: () => void
}
4. 类型推断 #
tsx
import { IntrinsicElementsKeys } from '@emotion/styled'
function createStyled<T extends IntrinsicElementsKeys>(element: T) {
return styled(element)`
box-sizing: border-box;
`
}
const StyledDiv = createStyled('div')
const StyledButton = createStyled('button')
下一步 #
掌握了 TypeScript 集成后,继续学习 响应式设计,了解如何实现响应式布局。
最后更新:2026-03-28