一、react框架为什么要和typescript配合使用

React 和 TypeScript 配合使用,本质上是因为 React 的组件化模型天然有大量"接口"需要被描述,而 TypeScript 恰好擅长描述接口。下面从几个层面说明。

1、React 的 props 就是天然的接口

React 组件最核心的输入是 props。没有 TypeScript 时,你只能靠注释或文档告诉别人"这个组件要传什么":

// 谁知道 title 是必填还是可选?children 是什么类型?
function Button(props) {
  return <button title={props.title}>{props.children}</button>;
}

用 TypeScript 后,props 变成了一份可执行的契约:

type Props = {
  className: string;
  title?: string;
  children: React.ReactNode;
};

function Button({ className, title, children }: Props) { ... }

好处是:

  • 调用方传错立刻报错:少传 className、多传一个不存在的 onClik(拼写错误),编辑器马上标红。

  • 编辑器智能提示:输入 <Button 后,自动补全所有可用属性,并提示每个属性的类型。

  • 重构安全:改一个 prop 名,所有使用处都会同步报错,不会漏改。

2、React 的生态本身就用 TS 写类型

React 官方类型定义(@types/react)非常完善。像 React.ReactNode、React.ChangeEvent、React.FC 这些类型,是社区多年沉淀的结果。你用 TS,就能直接享受:

function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
  console.log(e.target.value); // e.target.value 有类型,不会写错
}

如果不用 TS,e 是什么类型、e.target 有哪些属性,全靠记忆或查文档。

3、Hooks 的泛型让状态管理更安全

useState、useRef、useReducer 都支持泛型,这让状态类型可控:

// 不加类型:user 被推断为 null,之后没法赋值
const [user, setUser] = useState(null);

// 加类型:明确 user 是 User | null
const [user, setUser] = useState<User | null>(null);

// 这样后续 user.name 就会被 TS 拦住,除非先做非空判断
if (user) {
  console.log(user.name);
}

没有 TS 时,这类"null 上取属性"的错误往往要到运行时才暴露。

4、事件、ref、context 都有类型

React 里大量 API 是泛型的,TS 能帮你精确定义:

image

5、联合类型天然适合"状态机"式的 UI

React 组件常有多种状态:加载中、成功、失败、空。用联合类型可以强制你把每种情况都处理到:

type State =
  | { status: 'loading' }
  | { status: 'success'; data: User[] }
  | { status: 'error'; message: string };

function List({ state }: { state: State }) {
  switch (state.status) {
    case 'loading': return <Spinner />;
    case 'success': return <ul>{state.data.map(...)}</ul>;
    case 'error': return <p>{state.message}</p>;
  }
}

如果漏了某个 case,TS 会提示。这在纯 JS 里只能靠自觉。

6、大型项目里,TS 是"活的文档"

React 项目组件多了之后,最大的成本是理解别人(或几个月后的自己)写的组件。TS 让组件签名本身成为文档:

  • 看 type Props 就知道这个组件怎么用。

  • 看 useState<User | null> 就知道这个状态可能为空。

  • 看 onSubmit: (data: FormData) => Promise<void> 就知道回调的契约。

这比写注释更可靠,因为类型不对会报错,注释不会。

7、什么时候可以不用 TS?

也不是所有场景都必须用:

  • 一次性原型、demo:追求速度,JS 更快。

  • 非常小的脚本:引入 TS 的配置成本不划算。

  • 团队完全不熟悉 TS:强行上会降低效率。

但对于中长期维护、多人协作、组件库、有复杂状态的 React 项目,TS 的收益远大于成本。

小结:

React 和 TypeScript 配合好,根本原因是:

React 的特点TypeScript 的作用
组件化,props 是接口 描述并校验接口
Hooks 有泛型 让状态、ref、context 类型明确
事件系统复杂 提供精确的事件类型
UI 有多状态 联合类型强制穷尽处理
项目长期维护 类型即文档,重构安全

一句话:React 负责"怎么渲染",TypeScript 负责"数据长什么样",两者结合让组件既好用又不易出错。

React和TypeScript集合使用的重点集中在 和存储数据/状态有关的Hook函数 以及 组件接口 的位置,这些地方最需要数据类型校验

image

vite官网:https://cn.vitejs.dev/

Vite(法语意为 "快速的",发音 /viːt/发音同 "veet")是一种新型前端构建工具,能够显著提升前端开发体验。它主要由两部分组成:

二、使用Vite创建项目

Vite是一个框架无关的前端工具链工具,可以帮助我们快速创建一个 react+ts 的工程化环境出来,我们可以基于它做语法学习

create-vite 要求 Node.js 版本为 ^20.19.0 或 >=22.12.0

npm create vite@latest react-typescript -- --template react-ts

npm create vite@latest是固定命令,表示以最新的vite版本创建项目,react-typescript为项目名称

说明:

1.npmcreatevite@latest固定写法(使用最新版本vite初始化项目)

2.react-ts-pro项目名称(可以自定义)

3.----templatereact-ts:指定项目模版位react+ts

目录结构如下:

image

安装依赖运行项目

# 安装依赖
npm i 

# 运行项目
npm run dev

运行成功后,浏览器访问localhost:5173,如下所示:

image

下面进行代码的清理

src目录下只保留App.tsx和main.tsx

App.tsx中的代码清理后如下所示

function App() {
  return (
    <>
      this is app
    </>
  )
}
export default App

main.tsx中的代码如下所示:

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

由于严格模式StrictMode会让代码执行两次,我们先删除,清理代码后如下所示:

import { createRoot } from 'react-dom/client'
import App from './App.tsx'

createRoot(document.getElementById('root')!).render(
    <App />
)

 

三、Hooks与TypeScript

1、useState-自动推导

通常React会根据传入useState的默认值来自动推导类型,不需要显式标注类型

情况一、简单场景下,可以使用TS的自动推断机制,不用特殊编写类型注解,运行良好

const [val, toggle] = React.useState(false)

// `val` 会被自动推断为布尔类型
// `toggle` 方法调用时只能传入布尔类型

说明:

1.value:类型为boolean

2.toggle:参数类型为boolean

import {useState} from 'react'

function App() {
  const [value, toggle] = useState(false)
  const changeValue = ()=>{
    toggle(true)
  }
  const [list, setList] = useState([1,2,3])
  const changeList = ()=>{
    setList([...list, 4])
  }
  return (
    <>
      this is app{list}
    </>
  )
}
export default App

情况二、复杂数据类型,useState支持通过泛型参数指定初始参数类型以及setter函数的入参类型

useState本身是一个泛型函数,可以传入具体的自定义类型

说明:

1.限制useState函数参数的初始值必须满足类型为:User|()=>User(要么是User类型,要么是一个函数,返回的是User类型)

(1)、要么是User类型

const [user,setUser] = useState<User>({
    name: 'jack',
    age: 18
  })

(2)、要么是一个函数,返回的是User类型

const [user,setUser] = useState<User>(()=>{
    return {
      name: 'jack',
      age: 18
    }
  })

2.限制setUser函数的参数必须满足类型为:User|()=>User|undefined(要么是User类型,要么是一个函数,返回的是User类型,要么是undefined)

(1)、要么是User类型

const changeUser = () => {
    setUser({
      name: 'tom',
      age: 20
    })
  }

(2)、要么是一个函数,返回的是User类型

const changeUser = () => {
    setUser(()=>({
        name: 'jack',
        age: 18
      }))
  }

或者

const changeUser = () => {
    setUser(()=>{
      return {
        name: 'jack',
        age: 18
      }
    })
  }

(3)、要么返回undefined

const [user,setUser] = useState<User>()
  
  const changeUser = () => {
    setUser(undefined)
  }

3.user状态数据具备User类型相关的类型提示

import {useState} from 'react'

type User = {
  name: string
  age: number
}
function App() {
  const [user,setUser] = useState<User>(()=>{
    return {
      name: 'jack',
      age: 18
    }
  })
  
  const changeUser = () => {
    setUser(()=>({
        name: 'john',
        age: 28
      }))
  }
  return (
    <>
      this is app{user.name}
    </>
  )
}
export default App

情况三、初始值为null,没有具体默认值

实际开发时,有些时候useState的初始值可能为null或者undefined,按照泛型的写法是不能通过类型校验的,此时可以通过完整的类型联合null或者undefined类型即可

当我们不知道状态的初始值是什么,将useState的初始值为null是一个常见的做法,可以通过具体类型联合null来做显式注解
将来状态里面有一个具体的类型User,然后把这个类型User联合null放到泛型的位置,这样就可以把初始值设置为null。即一开始是null,将来是具体类型
type User = {
  name: String
  age: Number
}
const [user, setUser] = React.useState<User>(null)
// 上面会类型错误,因为null并不能分配给User类型

const [user, setUser] = React.useState<User | null>(null)
// 上面既可以在初始值设置为null,同时满足setter函数setUser的参数可以是具体的User类型

说明:

1.限制useState函数参数的初始值可以是User|null

const [user,setUser] = useState<User | null>(null)

2.限制setUser函数的参数类型可以是User|null

const changeUser = () => {
    setUser(null)
    setUser({
      name: 'john',
      age: 28
    }) 
  }

完整代码:

import {useState} from 'react'

type User = {
  name: string
  age: number
}
function App() {
  const [user,setUser] = useState<User | null>(null)
  const changeUser = () => {
    setUser(null)
    setUser({
      name: 'john',
      age: 28
    }) 
  }
  // 为了类型安全,可选链做类型守卫,只有user不为null的时候才进行点运算
  return (
    <>
      this is app{user?.name}
    </>
  )
}
export default App

 

2、useRef

useRef经常用来获取dom或者当成一个稳定的存储器来用,
(1)、获取dom的场景
可以直接把要获取的dom元素的类型当成泛型参数传递给useRef,可以推导出.current属性的类型
import {useRef,useEffect}  from 'react'
function App() {
  const domRef = useRef<HTMLInputElement>(null)
  useEffect(()=>{
    domRef.current?.focus()
  },[])
  return (
    <>
      <input ref={domRef}></input>
    </>
  )
}

export default App

效果如下:

image

我们想获取input输入框的dom,先通过useRef函数,通过泛型参数传递过来一个类型HTMLInputElement,也就是你想获取的元素是什么样的类型,就把它的类型当参数传过来,初始值可以设置为null。然后通过domRef.current获取dom元素。

在TypeScript的环境下,useRef 函数返回一个只读 或者 可变 的引用,只读的场景常见于获取真实dom,可变的场景,常见于缓存一些数据,不跟随组件渲染,下面分俩种情况说明

(2)、稳定引用存储器

把useRef当成引用稳定的存储器使用的场景可以通过泛型传入联合类型来做,比如定时器的场景:

image

number为将来要存储的定时器ID的类型,将来在组件销毁的时候清除定时器

当做为可变存储容器使用的时候,可以通过泛型参数指定容器存入的数据类型, 在还为存入实际内容时通常把null作为初始值,所以依旧可以通过联合类型做指定

interface User {
  age: number
}

function App(){
  const timerRef = useRef<number | undefined>(undefined)
  const userRes = useRef<User | null> (null)
  useEffect(()=>{
    timerRef.current = window.setInterval(()=>{
      console.log('测试')
    },1000)
    
    
    return ()=>clearInterval(timerRef.current)
  })
  return <div> this is app</div>
}

四、Component与TypeScript

基础使用:如何在组件中注解props

1、为Props添加类型

为组件prop添加类型,本质是给函数的参数做类型注解,可以使用type对象类型或者interface接口来做注解

image

说明:Button组件只能传入名称为className的prop参数,类型为string,且为必填
(1)、使用type类型来做注解
type Props = {
  className: string
}
function Button(props: Props) {
  const {className} = props
  return <button className={className}>click me</button>
}
function App() {
  // 为了类型安全,可选链做类型守卫,只有user不为null的时候才进行点运算
  return (
    <>
      <Button className='test'/>
    </>
  )
}
export default App

效果如下:

image

 (2)、使用interface接口来做注解

interface Props {
  className: string,
  title?: string // ?表示可选
}
function Button(props: Props) {
  const {className,title} = props
  return <button className={className} title={title}>click me</button>
}
function App() {
  // 为了类型安全,可选链做类型守卫,只有user不为null的时候才进行点运算
  return (
    <>
      <Button className='test' title="this is title"/>
    </>
  )
}
export default App

效果如下:

image

props作为React组件的参数入口,添加了类型之后可以限制参数输入以及在使用props有良好的类型提示

项目实战:

案例一:指定请求参数的类型

(1)、在src/types/index.ts中定义interface接口并导出

/** 登录表单 */
export interface LoginForm {
  mobile: string // 没有 ?,说明都是必填
  code: string // 没有 ?,说明都是必填
}

(2)、在Login/index.tsx文件中引入

引入LoginForm接口

import type { LoginForm } from '@/types'

使用LoginForm接口:登录表单的提交处理函数

const onFinish = async (formValue: LoginForm) => { // 参数名叫 formValue,类型是 LoginForm
    // await 的作用是暂停当前函数的执行,等这个 Promise 完成,
    // 等请求真正结束后,才继续执行后面的 navigate('/') 和 message.success
    await dispatch(fetchLogin(formValue))
    navigate('/')
    message.success('登录成功')
  }

案例二:指定接口统一返回结构的类型

(1)、在src/types/index.ts中定义interface接口并导出

/** 接口统一返回结构:{ message, data } */
// 为什么需要泛型?不同接口返回的 data 结构完全不同,泛型让 data 的类型由使用方决定。
// 当你不传泛型参数时,T 就是 unknown
export interface ApiResponse<T = unknown> { // T 是类型变量,默认值是 unknown
  message?: string // 有 ?,说明不必填
  data: T
}

/** 频道 */
export interface Channel {
  id: number
  name: string
}

(2)、在Article/index.tsx文件中引入

引入

import type { ApiResponse, Article, ArticleListParams, Channel } from '@/types'

使用:

// 调用接口
  useEffect(() => {
    async function fetchChannels() {
      // 泛型嵌套使用:最外层ApiResponse<...>为接口统一返回结构 { message, data }
      // 内层{ channels: Channel[] },data 的具体结构,一个对象,含 channels 字段
      // 最内层Channel[],channels 是 Channel 类型的数组
      const res = await request.get<ApiResponse<{ channels: Channel[] }>>('/channels')
      setChannels(res.data.channels)
    }
    fetchChannels()
  }, [])

 

jsx代码

<Form.Item label="频道" name="channel_id">
            <Select
              placeholder="请选择文章频道"
              style={{ width: 120 }}
            >
              {channels.map(item => (
                <Option key={item.id} value={item.id}>
                  {item.name}
                </Option>
              ))}
            </Select>
          </Form.Item>

案例三:antd 的 Table 组件定义列配置

(1)、在src/types/index.ts中定义interface接口并导出

/** 文章封面 */
export interface ArticleCover {
  type: number
  images: string[]
}

/** 文章 */
export interface Article {
  id: string
  title: string
  status: number
  pubdate: string
  read_count: number
  comment_count: number
  like_count: number
  cover: ArticleCover
  channel_id?: number
  content?: string
}

(2)、在Article/index.tsx文件中引入

引入

import type { ApiResponse, Article, ArticleListParams, Channel } from '@/types'

使用

// 准备列数据
  // 如果不加这个泛型TableColumnsType<Article>,dataIndex 可以随便写,比如写成 'titel'(拼错),TypeScript 不会报错。
  const columns: TableColumnsType<Article> = [
    {
      title: '封面',
      dataIndex: 'cover',
      width: 120,
      // Article['cover'] 是索引访问类型,等价于 ArticleCover,这里显式标注参数类型
      render: (cover: Article['cover']) => {
        return <img src={cover.images[0] || img404} width={80} height={60} alt="" />
      }
    },
    {
      title: '标题',
      dataIndex: 'title',
      width: 220
    },
    {
      title: '状态',
      dataIndex: 'status',
      render: () => <Tag color="green">审核通过</Tag>
    },
    {
      title: '发布时间',
      dataIndex: 'pubdate'
    },
    {
      title: '阅读数',
      dataIndex: 'read_count'
    },
    {
      title: '评论数',
      dataIndex: 'comment_count'
    },
    {
      title: '点赞数',
      dataIndex: 'like_count'
    },
    {
      title: '操作',
      // 用 _ 开头表示第一个参数"故意不用"
      render: (_: unknown, data: Article) => {
        return (
          <Space size="middle">
            <Button type="primary" shape="circle" icon={<EditOutlined />} onClick={() => navigate(`/publish?id=${data.id}`)} />
            <Popconfirm
              title="确认删除该条文章吗?"
              onConfirm={() => delArticle(data)}
              okText="确认"
              cancelText="取消"
            >
              <Button
                type="primary"
                danger
                shape="circle"
                icon={<DeleteOutlined />}
              />
            </Popconfirm>
          </Space>
        )
      }
    }
  ]

类型 TableColumnsType<Article> 是 antd 提供的泛型,<Article> 告诉 TypeScript:每一行的数据类型是 Article。好处是 dataIndex 只能填 Article 里存在的字段名,填错会报错。

调接口时指定类型

// 删除回调
  async function delArticle (data: Article) {
    await delArticleApi(data.id)
    // 更新列表
    setParams({
      page: 1,
      per_page: 2
    })
  }

apis中的代码

export function delArticleApi (id: string) {
  return request<ApiResponse<null>>({
    url: `/mp/articles/${id}`,
    method: 'DELETE'
  })
}

案例四:

(1)、在src/types/index.ts中定义interface接口并导出

/** 文章列表查询参数 */
export interface ArticleListParams {
  page: number
  per_page: number
  begin_pubdate?: string | null
  end_pubdate?: string | null
  status?: number | string | null
  channel_id?: number | null
}

(2)、在Article/index.tsx文件中引入

import type { ApiResponse, Article, ArticleListParams, Channel } from '@/types'

使用

// 这行代码声明了一个文章列表查询参数的状态,用 useState 管理(状态变量一旦发生变化组件的视图UI也会跟着变化),类型是 ArticleListParams
  const [params, setParams] = useState<ArticleListParams>({
    page: 1,
    per_page: 2,
    begin_pubdate: null,
    end_pubdate: null,
    status: null,
    channel_id: null
  })
  // Partial<ArticleListParams>:类型ArticleListParams 的所有字段都变成可选
  // Partial 是 TypeScript 内置的工具类型,作用是把某个类型的所有字段变成可选
  // { ...params, ...reqData }这是对象展开合并,两个对象按顺序展开,后面的覆盖前面的
  async function getList (reqData: Partial<ArticleListParams> = {}) {
    const res = await getArticleListApi({ ...params, ...reqData })
    setList(res.data.results)
    setCount(res.data.total_count)
  }
const onFinish = async (formValue: FilterForm) => {
    // 1. 准备参数
    const { channel_id, date, status } = formValue
    // Partial 让所有字段可选,所以这个对象可以只写部分字段,不用把 page、per_page 也带上。
    const reqData: Partial<ArticleListParams> = {
      status: status === '' ? undefined : status,
      channel_id,
      begin_pubdate: date?.[0]?.format('YYYY-MM-DD'),
      end_pubdate: date?.[1]?.format('YYYY-MM-DD'),
    }
    // 2. 使用参数获取新的列表
    getList(reqData)
  }

案例五:PayloadAction中指定payload的类型

(1)、在src/types/index.ts中定义interface接口并导出

/** 用户信息 */
export interface UserInfo {
  id?: string
  name?: string
  mobile?: string
  photo?: string
}

(2)、store/modules/user.js中引入

import type { UserInfo } from '@/types'

使用

// 定义用户模块的状态类型
export interface UserState {
  token: string
  userInfo: UserInfo
}
//定义了 Redux 中用户模块初始状态
const initialState: UserState = {
  token: getToken() || '',
  userInfo: {}
}

const userStore = createSlice({
  name: 'user',
  // 数据状态
  initialState,
  // 同步修改方法
  reducers: {
    setToken (state, action: PayloadAction<string>) {
      state.token = action.payload
      // localStorage中存一份
      _setToken(action.payload)
    },
    // action: PayloadAction<UserInfo>动作对象,泛型 <UserInfo> 指定 action.payload 的类型
    setUserInfo (state, action: PayloadAction<UserInfo>) {
      state.userInfo = action.payload
    },

  }
})

 

 

2、为Props的chidren属性添加类型

children是一个比较特殊的prop,作为组件标签里面的内容,类似于插槽,支持多种不同类型数据的传入,需要通过一个内置的ReactNode类型来做注解

image

说明:注解之后,children可以是多种类型,包括:React.ReactElement、string、number、React.ReactFragment、React.ReactPortal、boolean、null、undefined

children属性和props中其他的属性不同,它是React系统中内置的,其它属性我们可以自由控制其类型,children属性的类型最好由React内置的类型提供,兼容多种类型

type Props = {
  className: string,
  children: React.ReactNode,
  title?: string,
}
function Button(props: Props) {
  const {className,title,children} = props
  return <button className={className} title={title}>{children}</button>
}
function App() {
  // 为了类型安全,可选链做类型守卫,只有user不为null的时候才进行点运算
  return (
    <>
      <Button className='test' title="this is title">点击我</Button>
    </>
  )
}
export default App

字符串“点击我”将被当成一个参数传递到children中,

效果:

image

说明:React.ReactNode是一个React内置的联合类型,包括 React.ReactElement 、string、number React.ReactFragment 、React.ReactPortal 、boolean、 null 、undefined

如果你希望组件严格只接受 JSX 元素,拒绝纯文本或数字,则使用 React.ReactElement

import React from 'react';
// 从 Layout 中解构出 Header
import { SearchOutlined } from '@ant-design/icons';
type Props = {
  className: string,
  children: React.ReactElement,
  title?: string,
}
// interface Props {
//   className: string,
//   title?: string // ?表示可选
// }
function Button(props: Props) {
  const {className,title,children} = props
  return <button className={className} title={title}>{children}</button>
}
function App() {
  // 为了类型安全,可选链做类型守卫,只有user不为null的时候才进行点运算
  return (
    <>
      <Button className='test' title="this is title">
        <SearchOutlined />
      </Button>
    </>
  )
}
export default App

效果如下:

image

3、为事件prop添加类型

组件经常执行类型为函数的prop实现子传父,把子组件中的数据传给父组件进行使用,这类prop重点在于函数参数类型的注解

image

先定义Props类型,里面写好了一个函数类型,命名规范为以on开头,后面跟上驼峰命名。注解好了传入的数据为string类型。接着把Props类型注解到props参数的位置,在内部点击按钮的时候执行传递过来的onGetMsg函数,

说明:

1.在组件内部调用时需要遵守类型的约束,参数传递需要满足要求

onGetMsg?.('this is msg')

传递的参数必须是string类型

2.绑定prop时如果绑定内联函数直接可以推断出参数类型,否则需要单独注解匹配的参数类型

image

鼠标放到msg上面会自动推断出为string类型,而不用写成下面的样子,即加:string

<Son onGetMsg={(msg:string) => console.log(msg)} />

3、如果不是通过内联绑定,而是单独绑定的,需要手动注解,如下所示:

function App() {
  const getMsgHandler = (msg: string) => {
    console.log(msg)
  }
  return (
    <>
      {/* <Son onGetMsg={(msg:string) => console.log(msg)} /> */}
      <Son onGetMsg={getMsgHandler} />
    </>
  )
}

此时类型推断会丢失,需要添加:string

完整代码:

// props + ts
type Props = {
  onGetMsg?: (msg: string) => void
}

function Son(props: Props) {
  const { onGetMsg } = props
  const clickHandler = () => {
    onGetMsg?.('this is msg')
  }
  return <button onClick={clickHandler}>sendMsg</button>
}

function App() {
  const getMsgHandler = (msg: string) => {
    console.log(msg)
  }
  return (
    <>
      <Son onGetMsg={(msg) => console.log(msg)} />
      <Son onGetMsg={getMsgHandler} />
    </>
  )
}

export default App

4、为事件handle添加类型

为事件回调添加类型约束需要使用React内置的泛型函数来做,比如最常见的鼠标点击事件和表单输入事件:

function App(){
  const changeHandler: React.ChangeEventHandler<HTMLInputElement> = (e)=>{
    console.log(e.target.value)
  }
  
  const clickHandler: React.MouseEventHandler<HTMLButtonElement> = (e)=>{
    console.log(e.target)
  }

  return (
    <>
      <input type="text" onChange={ changeHandler }/>
      <button onClick={ clickHandler }> click me!</button>
    </>
  )
}

五、将 CRA项目迁移到 React + TypeScript

需要先说明:CRA 本身已停止维护,官方文档也已标注其废弃状态。因此,主流做法是迁移到 Vite,而不是继续停留在 CRA 体系内。

CRA 的构建工具 react-scripts 已不活跃,Vite 是目前 React SPA 的官方推荐替代方案。社区也提供了专门的迁移工具来降低转换成本

你可以使用 viject 工具一次性完成基础转换:

cd 你的项目目录
npx viject

结果:

D:\project\react\react-jike2-cra2ts>npx viject
T  Start migrating...
|
o  Checking Git status: Done
|
o  Rewriting package.json: Done
|
o  Rewriting d.ts files: Done
|
o  Writing vite.config.js: Done
|
o  Moving index.html: Done
|
o  Converting JS to JSX: Done
|
o  Next steps ---+
|                |
|  // npm        |
|  npm install   |
|  npm run dev   |
|  // yarn       |
|  yarn install  |
|  yarn dev      |
|  // pnpm       |
|  pnpm install  |
|  pnpm dev      |
|                |
+----------------+
|
—  Done ⚡

它会自动帮你重写 npm 脚本、添加 Vite 依赖、移动 index.html、并生成 vite.config.ts。迁移后,你只需要将 .jsx 文件逐个重命名为 .tsx,然后逐步为组件添加类型即可

如果你希望从零开始一个干净的 Vite + React + TS 项目,可以直接用官方模板创建:

npm create vite@latest my-app -- --template react-ts

这会生成标准的 tsconfig.json、vite.config.ts 和 .tsx 入口文件。你可以把 CRA 项目里的 src 目录按文件逐步搬运过去。

 

posted on 2026-09-17 11:20  周文豪  阅读(5)  评论(0)    收藏  举报