类型安全是咱们在运用 TypeScript 时最大的优势,但是在 React 项目中,咱们怎么正确运用 TypeScript 呢,也许本文能给你带来一些帮助。

标注 React 组件的类型

假设咱们有这样一个 Card 组件,其包含了 title 特点用于显现标题 及 onClick 点击事情:

interface CardProps {
  className?: string
  children: React.ReactNode
  title?: string
  onClick?: (e: React.MouseEvent) => void
}
function Card(props: CardProps) {
  return (
    <div
      className="rounder-xl"
      onClick={props.onClick}
    >
      <span>{props.title}</span>
      {props.children}
    </div>
  )
}

怎么传递 className

通常咱们会为组件增加 className,在 React 中咱们需求凭借一些第三方库如 classnamesclsx 来处理:

function Card(props: CardProps) {
  const { className, children, ...rest } = props
  return (
    <div className={clsx('rounder-xl', className)}>
      <span>{rest.title}</span>
      {children}
    </div>
  )
}

怎么承继 HTML 原生标签的特点

咱们能够运用 React.HTMLAttributes 来承继 HTML 原生标签的特点:

// Bad ❌
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {}

更加推荐的方法是运用 React.ComponentProps 来承继 HTML 原生标签的特点:

// Good ✅
interface CardProps extends React.ComponentProps<'div'> {}

再具体一点,咱们能够一起指定是否暴露 ref 特点给外部:

interface CardProps extends React.ComponentPropsWithRef<'div'> {}
// Perfect ✅
interface CardProps extends React.ComponentPropsWithoutRef<'div'> {}

这样咱们就能够让 CardProps 具有一切 div 标签的特点,一起不暴露 ref 特点给外部。

interface CardProps extends React.ComponentPropsWithoutRef<'div'> {
  title?: string
}
function Card(props: CardProps) {
  const { className, children, title, ...rest } = props
  return (
    <div
      className={clsx('rounder-xl', className)}
      {...rest}
    >
      <span>{title}</span>
      {children}
    </div>
  )
}

CardProps 承继了 div 的特点后,经过 {...rest} 传参,咱们省去了手动绑定 onClick 事情。

注意:上面将 className 单独解构,是为了避免传入 className 覆盖了组件内部的 className

范型组件

动态指定标签类型

假如咱们需求动态指定 Card 组件最外层的标签类型:

type CardProps<T extends React.ElementType> =
  React.ComponentPropsWithoutRef<T> &
    React.PropsWithChildren<{
      tag?: T
      title?: string
    }>
function Card<T extends React.ElementType = 'div'>(props: CardProps<T>) {
  const { tag: Component = 'div', children, title, ...rest } = props
  return (
    <Component {...rest}>
      <span>{title}</span>
      {children}
    </Component>
  )
}

这样咱们能够经过 tag 特点操控 Card 组件最外层的标签类型,且能够经过范型来得到对应标签的类型提示。

此时默认 Card 最外层的标签为 div,咱们仅能够传递 div 的特点,假如将 tag 特点指定为 button,则能够传递 button 的特点,如 disabled,这个特点是不在 div 上的。

在 .tsx 文件中为箭头函数增加范型

.tsx 文件中为箭头函数增加范型时,假如仅有 T,需求在后面增加 , 避免范型被识别为 JSX 语法:

这样编译器无法识别:

const Card = <T>() => {}

这样就不会报错了:

const Card = <T,>() => {}

完成能够动态烘托的列表组件

interface CardListProps<T> {
  items: T[]
  renderItem: (item: T) => React.ReactNode
}
function CardList<T>(props: CardListProps<T>) {
  return (
    <div className="flex flex-col space-y-4">
      {props.items.map(props.renderItem)}
    </div>
  )
}

上述代码界说了一个 CardList 组件,其承受一个 items 数组和一个 renderItem 函数,用于烘托列表中的每一项。

依据咱们的数据类型进行烘托:

interface Framework {
  id: number
  name: string
}
const data = [
  { id: 1, name: 'React' },
  { id: 2, name: 'Vue' },
  { id: 3, name: 'Angular' }
]
const App = () => {
  return (
    <CardList<Framework>
      items={data}
      renderItem={(framework) => {
        return (
          <div>
            <span>{framework.id}</span>
            <span>{framework.name}</span>
          </div>
        )
      }}
    />
  )
}

上述代码咱们能够经过传入 Framework 类型来指定 CardList 组件的数据类型,renderItem 函数也会依据 Framework 类型拿到对应的参数类型。

这是一个常见的 React 进阶技术,叫做 Render Props,这方便了咱们在父组件中进行自界说烘托,一起也完成了组件的关注点分离。在 React Native 中,FlatList 组件便是采用了这样的方法进行了封装。

类型缩窄

假如需求依据特点条件烘托组件,咱们能够基于 TypeScript 的类型缩窄来完成:

interface ButtonProps {
  text?: string
}
interface LinkProps {
  href?: string
  text?: string
}
function isLinkProps(props: ButtonProps | LinkProps): props is LinkProps {
  return 'href' in props
}
function Clickable(props: ButtonProps | LinkProps) {
  if (isLinkProps(props)) {
    // 此处的 Props 类型为 LinkProps
    return <a href={props.href}>{props.text}</a>
  } else {
    // 此处的 Props 类型为 ButtonProps
    return <button>{props.text}</button>
  }
}

上述代码能够依据是否传入 href 特点来烘托 a 标签或 button 标签。

事情处理

咱们能够经过 React.MouseEventHandlerReact.ChangeEventHandler 来界说事情处理的类型:

interface DemoProps {
  onClick: React.MouseEventHandler<HTMLButtonElement>
  onChange: React.ChangeEventHandler<HTMLInputElement>
}
// Good ✅
const handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
  console.log(e.currentTarget)
}
// Good ✅
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
  console.log(e.currentTarget.value)
}
// Bad ❌
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
  console.log(e.currentTarget)
}
// Bad ❌
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  console.log(e.currentTarget.value)
}
function Demo(props: DemoProps) {
  return (
    <div>
      <button onClick={props.onClick}>Click</button>
      <input onChange={props.onChange} />
    </div>
  )
}

运用 React.MouseEventReact.ChangeEvent 并不推荐,它们仅仅只能限定事情的参数类型。