TS React 项目中使用TypeScript
在 React 项目中使用 TS
- 创建新项目
- 在现有项目中添加 TS
创建新项目
- 命令:
npx create-react-app my-app --template typescript
- 说明:在命令行中,添加
--template typescript
表示创建支持 TS 的项目
- 项目目录的变化:
-
- 在项目根目录中多了一个文件:
tsconfig.json
- 在项目根目录中多了一个文件:
-
-
- TS 的配置文件
-
-
- 在 src 目录中,文件的后缀有变化,由原来的 .js 变为
.ts
或.tsx
- 在 src 目录中,文件的后缀有变化,由原来的 .js 变为
-
-
.ts
ts 文件的后缀名
-
-
-
.tsx
是在 TS 中使用 React 组件时,需要使用该后缀
-
-
- 在 src 目录中,多了
react-app-env.d.ts
文件
- 在 src 目录中,多了
-
-
.d.ts
类型声明文件,用来指定类型
-
基本使用
创建类组件
在vscode中通过tsrcc
快速创建类组件
import React, { Component } from 'react'
type Props = {}
type State = {}
export default class App extends Component<Props, State> {
state = {}
render() {
return (
<div>App</div>
)
}
}
其中泛型Props指外部数据的数据类型
State指内部数据的数据类型。
创建函数组件
定义函数组件第一种方式:
在vscode中通过tsrfc
快速创建函数组件
import React from 'react'
type Props = {}
export default function Header({}: Props) {
return (
<div>Header</div>
)
}
Props:指外部数据的数据类型
第二种方式:
import { FC } from 'react';
type Props = {
}
// FC:函数组件
const Nav: FC<Props> = function() {
return <div></div>
}
外部数据
简单的数据类型定义
import React, { Component } from 'react'
type Props = {
msg: string
}
type State = {}
export default class Footer extends Component<Props, State> {
state = {}
render() {
return (
<div>
消息: {this.props.msg}
</div>
)
}
}
复杂数据类型定义
定义复杂数据类型后,可以导出数据类型方便其他组件引入使用。
import React, { Component } from 'react'
export interface User {
name: string,
age: number
}
export type UserList = User[];
type Props = {
msg: string,
user: User,
userList: UserList
}
type State = {}
export default class Footer extends Component<Props, State> {
state = {}
render() {
return (
<div>
消息: {this.props.msg}
<br />
姓名:{this.props.user.name}
年龄:{this.props.user.age}
</div>
)
}
}
在父组件引入数据类型使用
import React, { Component } from 'react'
import Footer, { User, UserList } from './components/Footer'
type Props = {}
type State = {}
const user: User = {name: '张三', age: 20}
const userList: UserList = [{name: '李四', age: 30}];
export default class App extends Component<Props, State> {
state = {}
render() {
return (
<div>
<Footer msg={'消息'} user={user} userList={userList} />
</div>
)
}
}
内部数据
类组件的内部数据State
内部数据通过泛型传入State数据类型。后续使用中提示更加友好。
import React, { Component } from 'react'
type Props = {
}
type State = {
address: string
}
export default class Footer extends Component<Props, State> {
state = {
address: '红旗河沟'
}
changeAddr = () => {
this.setState({
address: '渝北区'
})
}
render() {
return (
<div>
地址:{this.state.address}
<button onClick={this.changeAddr}>修改地址</button>
</div>
)
}
}
函数组件的内部数据State
在函数组件中通过useState
创建内部数据
在创建某些复杂数据时,要注意显示去传入state的泛型数据类型,否则数据类型很容容易报错。
import React, { useEffect } from 'react'
import { useState } from 'react';
type Props = {
}
interface User {name: string, age: number}
export default function Header(props: Props) {
let [count, setCount] = useState(0);
let [user, setUser] = useState<User>({} as any);
let [userList, setUserList] = useState<User[]>([]);
function changeCount() {
setCount(10);
}
function changeUserList() {
setUserList([{
name: '张三',
age: 20
}]);
}
return (
<div>
count:{count}
<button onClick={changeCount}>修改count</button>
<br />
姓名:{user.name}
年龄:{user.age}
<button onClick={changeUserList}>修改userList</button>
</div>
)
}
对父子通信进行类型限定
首先让脚手架支持TypeScript,可以在安装脚手架的时候进行配置即可,命令如下。
npx create-react-app react-ts-study --template typescript
然后就是创建两个组件,并且完成props通信。
import React from 'react'
interface WelcomeProps {
msg?: string
count?: number
list: string[]
info: { username: string; age: number }
status?: 'loading' | 'success' | 'error'
}
function Welcome(props: WelcomeProps) {
const { count = 0 } = props;
return (
<div>
<h2>hello Welcome, {count}</h2>
</div>
)
}
export default function App() {
return (
<div>
<h2>01_react-ts</h2>
<Welcome msg="hello" count={123} list={['a', 'b', 'c']} info={{username: 'xiaoming', age: 20}} />
<Welcome list={['a', 'b', 'c']} info={{username: 'xiaoming', age: 20}} />
<Welcome status="loading" list={['a', 'b', 'c']} info={{username: 'xiaoming', age: 20}} />
</div>
)
}
下面来看一下函数表达式写法的情况下,如何指定props的类型,可通过内置的FC类型来进行实现。
const Welcome: React.FC<WelcomeProps> = (props) => {
return (
<div>
<h2>hello Welcome</h2>
</div>
)
}
children与event限制
children的类型限制
父子通信时候的内容分发进行限制。
import React from 'react'
interface WelcomeProps {
children?: React.ReactNode
}
function Welcome(props: WelcomeProps) {
return (
<div>
<h2>hello Welcome, {props.children}</h2>
</div>
)
}
export default function App() {
return (
<div>
<h2>02_react-ts</h2>
<Welcome />
<Welcome>
aaaaa
</Welcome>
</div>
)
}
我们把children属性作为可选参数,这样当<Welcome>组件进行内容分发和不进行内容分发都是可以的。
event限制
event在React中主要通过内置的ev: React.MouseEvent<HTMLButtonElement>来进行限定。
import React from 'react'
interface WelcomeProps {
children?: React.ReactNode
handleMsg?: (ev: React.MouseEvent<HTMLButtonElement>)=> void
}
function Welcome(props: WelcomeProps) {
const handleChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
console.log(ev.target.value)
}
return (
<div>
<h2>hello Welcome, {props.children}</h2>
<button onClick={props.handleMsg}>点击</button>
<input type="text" onChange={handleChange} />
</div>
)
}
export default function App() {
return (
<div>
<h2>02_react-ts</h2>
<Welcome />
<Welcome handleMsg={(ev)=>{}}>
aaaaa
</Welcome>
</div>
)
}
props.children问题
在tsx中props中要访问children,那么应该使用PropsWithChildren
去定义props数据类型
import React, { useEffect } from 'react'
import { useState, PropsWithChildren } from 'react';
type Props = {
}
export default function Header(props: PropsWithChildren<Props>) {
return (
<div
{props.children}
</div>
)
}
PropsWithChildren是一个数据类型,接口泛型Props数据类型,然后得到一个注入了children数据类型的Props数据类型。
通过FC创建的函数组件的props也没有children
属性,也需要使用PropsWithChildren
去定义
import { FC, PropsWithChildren } from 'react';
type Props = {
}
// FC:函数组件
const Nav: FC<PropsWithChildren<Props>> = function(props) {
return <div>{props.children}</div>
}
style与component限制
style限制
当我们进行style样式通信的时候,也是可以指定类型,防止样式传递的时候不复合规范。
import React from 'react'
interface HeaderProps {
username: string
}
interface WelcomeProps {
style: React.CSSProperties
}
function Welcome(props: WelcomeProps) {
return (
<div>
<h2>hello Welcome</h2>
</div>
)
}
export default function App() {
return (
<div>
<h2>03_react-ts</h2>
<Welcome style={{'border': '1px red solid', display: 'none'}} />
</div>
)
}
主要通过React.CSSProperties来指定样式的类型,这样当传递的样式属性或者值不符合规范的时候,就不会产生TS的提示。
component限制
如果组件进行通信的时候,也可以进行类型的限制。
import React from 'react'
interface HeaderProps {
username: string
}
interface WelcomeProps {
style: React.CSSProperties
component: React.ComponentType<HeaderProps>
}
function Welcome(props: WelcomeProps) {
return (
<div>
<h2>hello Welcome</h2>
<props.component username="xiaoming"></props.component>
</div>
)
}
function Header(props: HeaderProps) {
return (
<div>hello Header</div>
)
}
export default function App() {
return (
<div>
<h2>03_react-ts</h2>
<Welcome style={{'border': '1px red solid', display: 'none'}} component={Header} />
</div>
)
}
主要通过React.ComponentType<>来指定组件的类型,那么一旦不符合指定的接口类型,就会报错。
use函数限制
在React函数组件中,主要就是对use函数进行类型的注解。常见的注解use函数如下:
- useState -> 联合类型、对象字面量类型
- useEffect -> 自动类型推断
- useRef -> 泛型标签类型
import React, { useEffect, useState, useRef } from 'react'
interface WelcomeProps {
}
function Welcome(props: WelcomeProps) {
return (
<div>
<h2>hello Welcome</h2>
</div>
)
}
type Info = {username: string; age: number}
export default function App() {
//const [count, setCount] = useState(0)
const [count, setCount] = useState<number|string>(0)
const [list, setList] = useState<string[]>([])
//const [info, setInfo] = useState<{username: string; age: number}|null>(null)
const [info, setInfo] = useState<Info>({} as Info)
const myRef = useRef<HTMLButtonElement>(null)
useEffect(()=>{
console.log( myRef.current?.innerHTML ) // 可选链(类型保护)
//console.log( myRef.current!.innerHTML ) // 非空断言(慎用)
return ()=>{
}
}, [])
const handleClick = () => {
setCount(1)
setList(['a', 'b'])
}
return (
<div>
<h2>04_react-ts</h2>
<button onClick={handleClick} ref={myRef}>点击</button>
{ info.username }, { info.age }
<Welcome />
</div>
)
}
useState和useRef都是通过泛型的方式进行类型注解,useEffect主要利用自动类型推断来完成。
类组件类型限制
类组件在React中并不是重点,但是也要了解怎么对类组件进行类型的限制。
import React, { Component } from 'react'
interface WelcomeProps {
msg: string
count: number
}
interface WelcomeState {
username: string
}
class Welcome extends Component<WelcomeProps, WelcomeState> {
state = {
username: 'xiaoming'
}
render() {
return (
<div>hello Welcome {this.state.username}</div>
)
}
}
export default function App() {
return (
<div>
<h2>05_react-ts</h2>
<Welcome msg="hello" count={123} />
</div>
)
}
主要就是给继承的类Component传递泛型,Props和State,这样可以实现父子通信的数据进行类型限制,又可以对内部的state进行类型限制。
路由如何使用TS进行开发
react-router-dom类型限制
React路由与TS配合常见的使用为以下这些操作:
- RouteObject 内置类型,限制路由表
- React.createElement() 进行组件编写
- 扩展 meta 元信息
// /router/index.ts
import { createBrowserRouter } from 'react-router-dom'
import type { RouteObject } from 'react-router-dom'
import App from '../App';
import Index from '../views/Index/Index';
import User from '../views/User/User';
import Login from '../views/Login/Login';
import React from 'react';
declare module 'react-router' {
interface NonIndexRouteObject {
meta?: { title: string }
}
interface IndexRouteObject {
meta?: { title: string }
}
}
export const routes: RouteObject[] = [
{
path: '/',
element: React.createElement(App),
meta: { title: '/' },
children: [
{
path: 'index',
element: React.createElement(Index),
meta: { title: 'index' }
},
{
path: 'user',
element: React.createElement(User),
meta: { title: 'user' }
},
{
path: 'login',
element: React.createElement(Login)
}
]
}
];
const router = createBrowserRouter(routes);
export default router;
状态管理如何使用TS进行开发
Redux Toolkit限制类型
Redux状态管理与TS配合常见的使用为以下这些操作:
- 得到全局state类型: ReturnType<typeof store.getState>
- 限定payload类型: PayloadAction
// /store/index.ts
import { configureStore } from '@reduxjs/toolkit'
import userReducer from './modules/user';
import { useDispatch } from 'react-redux'
const store = configureStore({
reducer: {
user: userReducer
}
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
export const useAppDispatch: () => AppDispatch = useDispatch
export default store;
// /store/modules/user.ts
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
export const loginAction = createAsyncThunk(
'users/loginAction',
async (userId: number) => {
const response = await new Promise((resolve)=>{
resolve('response data')
})
return response
}
)
const userSlice = createSlice({
name: 'user',
initialState: {
name: 'xiaoming'
},
reducers: {
change(state, action: PayloadAction<string>){
state.name = action.payload
}
}
})
export const { change } = userSlice.actions
export default userSlice.reducer
tsconfig的介绍
- tsconfig.json是typescript项目的配置文件,用于配置typescript
- tsconfig.json配置文件可以通过
tsc --init
生成
- 说明:所有的配置项都可以通过鼠标移入的方式,来查看配置项的解释说明。
- tsconfig 文档链接
{
// 编译选项
"compilerOptions": {
// 生成代码的语言版本:将我们写的 TS 代码编译成哪个版本的 JS 代码
// 命令行: tsc --target es5 11-测试TS配置文件.ts
"target": "es5",
// 指定要包含在编译中的 library
"lib": ["dom", "dom.iterable", "esnext"],
// 允许 ts 编译器编译 js 文件
"allowJs": true,
// 跳过类型声明文件的类型检查
"skipLibCheck": true,
// es 模块 互操作,屏蔽 ESModule 和 CommonJS 之间的差异
"esModuleInterop": true,
// 允许通过 import x from 'y' 即使模块没有显式指定 default 导出
"allowSyntheticDefaultImports": true,
// 开启严格模式
"strict": true,
// 对文件名称强制区分大小写
"forceConsistentCasingInFileNames": true,
// 为 switch 语句启用错误报告
"noFallthroughCasesInSwitch": true,
// 生成代码的模块化标准
"module": "esnext",
// 模块解析(查找)策略
"moduleResolution": "node",
// 允许导入扩展名为.json的模块
"resolveJsonModule": true,
// 是否将没有 import/export 的文件视为旧(全局而非模块化)脚本文件
"isolatedModules": true,
// 编译时不生成任何文件(只进行类型检查)
"noEmit": true,
// 指定将 JSX 编译成什么形式
"jsx": "react-jsx"
},
// 指定允许 ts 处理的目录
"include": ["src"]
}
typescript声明文件
今天几乎所有的 JavaScript 应用都会引入许多第三方库来完成任务需求。
这些第三方库不管是否是用 TS 编写的,最终都要编译成 JS 代码,才能发布给开发者使用。
我们知道是 TS 提供了类型,才有了代码提示和类型保护等机制。
但在项目开发中使用第三方库时,你会发现它们几乎都有相应的 TS 类型,这些类型是怎么来的呢? 类型声明文件
- 类型声明文件:用来为已存在的 JS 库提供类型信息
这样在 TS 项目中使用这些库时,就像用 TS 一样,都会有代码提示、类型保护等机制了。
- TS 的两种文件类型
- 类型声明文件的使用说明
TS 中的两种文件类型
- TS 中有两种文件类型:1
.ts
文件 2.d.ts
文件
- .ts 文件:
-
既包含类型信息又可执行代码
- 可以被编译为 .js 文件,然后,执行代码
- 用途:编写程序代码的地方
- .d.ts 文件:
-
只包含类型信息
的类型声明文件- 不会生成 .js 文件,仅用于提供类型信息,在.d.ts文件中不允许出现可执行的代码,只用于提供类型
- 用途:为 JS 提供类型信息
- 总结:.ts 是
implementation
(代码实现文件);.d.ts 是 declaration(类型声明文件)
- 如果要为 JS 库提供类型信息,要使用
.d.ts
文件
类型声明文件的使用说明
- 在使用 TS 开发项目时,类型声明文件的使用包括以下两种方式:
-
- 使用已有的类型声明文件
- 创建自己的类型声明文件
使用已有的类型声明文件
- 内置类型声明文件
- 第三方库的类型声明文件
- 自己提供的
内置类型声明文件
- TS 为 JS 运行时可用的所有标准化内置 API 都提供了声明文件
- 比如,在使用数组时,数组所有方法都会有相应的代码提示以及类型信息:
const strs = ['a', 'b', 'c']
// 鼠标放在 forEach 上查看类型
strs.forEach
- 实际上这都是 TS 提供的内置类型声明文件
- 可以通过 Ctrl + 鼠标左键(Mac:Command + 鼠标左键)来查看内置类型声明文件内容
- 比如,查看 forEach 方法的类型声明,在 VSCode 中会自动跳转到
lib.es5.d.ts
类型声明文件中
- 当然,像 window、document 等 BOM、DOM API 也都有相应的类型声明(
lib.dom.d.ts
)
第三方库的类型声明文件
- 目前,几乎所有常用的第三方库都有相应的类型声明文件
- 第三方库的类型声明文件有两种存在形式:1 库自带类型声明文件 2 由 DefinitelyTyped 提供。
- 库自带类型声明文件:比如,axios
-
- 查看
node_modules/axios
目录
- 查看
解释:这种情况下,正常导入该库,TS 就会自动加载库自己的类型声明文件,以提供该库的类型声明。
- 由 DefinitelyTyped 提供
- DefinitelyTyped 是一个 github 仓库,用来提供高质量 TypeScript 类型声明
- DefinitelyTyped 链接
- 可以通过 npm/yarn 来下载该仓库提供的 TS 类型声明包,这些包的名称格式为:
@types/*
- 比如,@types/react、@types/lodash 等
- 说明:在实际项目开发时,如果你使用的第三方库没有自带的声明文件,VSCode 会给出明确的提示
import _ from 'lodash'
// 在 VSCode 中,查看 'lodash' 前面的提示
- 解释:当安装
@types/*
类型声明包后,TS 也会自动加载该类声明包,以提供该库的类型声明
- 补充:TS 官方文档提供了一个页面,可以来查询 @types/* 库
- @types/* 库
创建自己的类型声明文件
- 项目内共享类型
- 为已有 JS 文件提供类型声明
项目内共享类型
- 如果多个 .ts 文件中都用到同一个类型,此时可以创建 .d.ts 文件提供该类型,实现类型共享。
- 操作步骤:
-
- 创建 index.d.ts 类型声明文件。
- 创建需要共享的类型,并使用 export 导出(TS 中的类型也可以使用 import/export 实现模块化功能)。
- 在需要使用共享类型的 .ts 文件中,通过 import 导入即可(.d.ts 后缀导入时,直接省略)。
为已有 JS 文件提供类型声明
- 在将 JS 项目迁移到 TS 项目时,为了让已有的 .js 文件有类型声明。
- 成为库作者,创建库给其他人使用。
- 注意:类型声明文件的编写与模块化方式相关,不同的模块化方式有不同的写法。但由于历史原因,JS 模块化的发展 经历过多种变化(AMD、CommonJS、UMD、ESModule 等),而 TS 支持各种模块化形式的类型声明。这就导致 ,类型声明文件相关内容又多又杂。
- 演示:基于最新的 ESModule(import/export)来为已有 .js 文件,创建类型声明文件。
类型声明文件的使用说明
- 说明:TS 项目中也可以使用 .js 文件。
- 说明:在导入 .js 文件时,TS 会自动加载与 .js 同名的 .d.ts 文件,以提供类型声明。
- declare 关键字:用于类型声明,为其他地方(比如,.js 文件)已存在的变量声明类型,而不是创建一个新的变量。
-
- 对于 type、interface 等这些明确就是 TS 类型的(只能在 TS 中使用的),可以省略 declare 关键字。
- 对于 let、function 等具有双重含义(在 JS、TS 中都能用),应该使用 declare 关键字,明确指定此处用于类型声明。
let count = 10
let songName = '痴心绝对'
let position = {
x: 0,
y: 0
}
function add(x, y) {
return x + y
}
function changeDirection(direction) {
console.log(direction)
}
const fomartPoint = point => {
console.log('当前坐标:', point)
}
export { count, songName, position, add, changeDirection, fomartPoint }
定义类型声明文件
declare let count:number
declare let songName: string
interface Position {
x: number,
y: number
}
declare let position: Position
declare function add (x :number, y: number) : number
type Direction = 'left' | 'right' | 'top' | 'bottom'
declare function changeDirection (direction: Direction): void
type FomartPoint = (point: Position) => void
declare const fomartPoint: FomartPoint
export {
count, songName, position, add, changeDirection, FomartPoint, fomartPoint
}