你可能不知道的 React + TypeScript 常用技巧

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

标注 React 组件的类型

假设我们有这样一个 Card 组件,其包含了 title 属性用于显示标题 及 onClick 点击事件:

tsx 复制代码
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 来处理:

tsx 复制代码
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 原生标签的属性:

tsx 复制代码
// Bad ❌
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {}

更加推荐的方式是使用 React.ComponentProps 来继承 HTML 原生标签的属性:

tsx 复制代码
// Good ✅
interface CardProps extends React.ComponentProps<'div'> {}

再具体一点,我们可以同时指定是否暴露 ref 属性给外部:

tsx 复制代码
interface CardProps extends React.ComponentPropsWithRef<'div'> {}

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

这样我们就可以让 CardProps 具有所有 div 标签的属性,同时不暴露 ref 属性给外部。

tsx 复制代码
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 组件最外层的标签类型:

tsx 复制代码
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 语法:

这样编译器无法识别:

tsx 复制代码
const Card = <T>() => {}

这样就不会报错了:

tsx 复制代码
const Card = <T,>() => {}

实现可以动态渲染的列表组件

tsx 复制代码
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 函数,用于渲染列表中的每一项。

根据我们的数据类型进行渲染:

tsx 复制代码
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 的类型缩窄来实现:

tsx 复制代码
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 来定义事件处理的类型:

tsx 复制代码
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 并不推荐,它们仅仅只能限定事件的参数类型。

相关推荐
小信丶21 分钟前
解决 pnpm dev 报错:系统禁止运行脚本的问题
前端·vue.js·windows·npm
૮・ﻌ・28 分钟前
Vue3:组合式API、Vue3.3新特性、Pinia
前端·javascript·vue3
前端不太难29 分钟前
RN + TypeScript 项目越写越乱?如何规范架构?
前端·javascript·typescript
神算大模型APi--天枢64629 分钟前
全栈自主可控:国产算力平台重塑大模型后端开发与部署生态
大数据·前端·人工智能·架构·硬件架构
苏打水com30 分钟前
第十五篇:Day43-45 前端性能优化进阶——从“可用”到“极致”(对标职场“高并发场景优化”需求)
前端·css·vue·html·js
JS_GGbond36 分钟前
用美食来理解JavaScript面向对象编程
开发语言·javascript·美食
@大迁世界37 分钟前
08.CSS if() 函数
前端·css
Moment44 分钟前
小米不仅造车,还造模型?309B参数全开源,深度思考完胜DeepSeek 🐒🐒🐒
前端·人工智能·后端
苏打水com1 小时前
第十六篇:Day46-48 前端安全进阶——从“漏洞防范”到“安全体系”(对标职场“攻防实战”需求)
前端·javascript·css·vue.js·html
5C241 小时前
从思想到实践:前端工程化体系与 Webpack 构建架构深度解析
前端·前端工程化