一、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 能帮你精确定义:

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 负责"数据长什么样",两者结合让组件既好用又不易出错。

vite官网:https://cn.vitejs.dev/
Vite(法语意为 "快速的",发音 /viːt/发音同 "veet")是一种新型前端构建工具,能够显著提升前端开发体验。它主要由两部分组成:
-
一个开发服务器,它基于 原生 ES 模块 提供了 丰富的内建功能,如速度快到惊人的 模块热替换(HMR)。
-
一套构建指令,它使用 Rolldown 打包你的代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态资源。
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

# 安装依赖
npm i
# 运行项目
npm run dev
运行成功后,浏览器访问localhost:5173,如下所示:

下面进行代码的清理
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、-自动推导
情况一、简单场景下,可以使用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
说明:
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
实际开发时,有些时候useState的初始值可能为null或者undefined,按照泛型的写法是不能通过类型校验的,此时可以通过完整的类型联合null或者undefined类型即可
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、
import {useRef,useEffect} from 'react'
function App() {
const domRef = useRef<HTMLInputElement>(null)
useEffect(()=>{
domRef.current?.focus()
},[])
return (
<>
<input ref={domRef}></input>
</>
)
}
export default App
效果如下:

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

number为将来要存储的定时器ID的类型,将来在组件销毁的时候清除定时器
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、

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
效果如下:

(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
效果如下:

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、

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中,
效果:

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
效果如下:

3、

先定义Props类型,里面写好了一个函数类型,命名规范为以on开头,后面跟上驼峰命名。注解好了传入的数据为string类型。接着把Props类型注解到props参数的位置,在内部点击按钮的时候执行传递过来的onGetMsg函数,
说明:
1.在组件内部调用时需要遵守类型的约束,参数传递需要满足要求
onGetMsg?.('this is msg')
传递的参数必须是string类型
2.绑定prop时如果绑定内联函数直接可以推断出参数类型,否则需要单独注解匹配的参数类型

鼠标放到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、
为事件回调添加类型约束需要使用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 目录按文件逐步搬运过去。
浙公网安备 33010602011771号