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