React老项目迁移Typescript(持续更新ing)
前言
Typescript提供了类型校验和代码提示功能,在大型项目中引入Typescript还是很有必要的
这里从0到1介绍老的React项目引入Typescript的过程,以及可能遇到的问题和解决思路
本文后续会持续更新,码字不易,点赞支持!!!
一、安装
npm i typescript @types/react @types/react-dom @types/node @types/jest
二、配置
项目根目录下新建tsconfig.json
{
  "compilerOptions": {
    "target": "es5", // 目标语言的版本
    "lib": ["dom", "dom.iterable", "esnext", "ScriptHost"],
    // 编译时引入的 ES 功能库,包括:es5 、es6、es7、dom 等。
    // 如果未设置,则默认为: target 为 es5 时: ["dom", "es5", "scripthost"]
    //target 为 es6 时: ["dom", "es6", "dom.iterable", "scripthost"]
    "allowJs": true, // 允许编译器编译JS,JSX文件
    "checkJs":true, //对JS文件进行检查
    "skipLibCheck": true,
    "esModuleInterop": true, // 允许export=导出,由import from 导入
    "allowSyntheticDefaultImports": true,//允许使用 import xxx from 'xxx' 的形式来引入模块
    "strict": true, // 开启所有严格的类型检查
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true, // 防止switch语句贯穿(即如果没有break语句后面不会执行)
    "module": "esnext", // 生成代码的模板标准
    "moduleResolution": "node", // 模块解析策略,ts默认用node的解析策略,即相对的方式导入
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true, // 不输出文件,即编译后不会生成任何js文件
    "jsx": "react-jsx"
  },
  "include": ["src", "**/*.ts", "**/*.tsx"],
  "exclude": ["./node_modules"]
}
通过配置"checkJs":true, 对JS文件进行检查。可以通过在 .js 文件顶部添加 // @ts-nocheck 注释,让编译器忽略当前文件的类型检查
三、路径映射
情景:使用了路径别名的引入有红色波浪线,提示找不到模块"@/XXX"或其相应的类型声明
方案:配置path解析非相对路径
根目录下新建tsconfig.paths.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "less/*": ["./src/assets/styles/less/*"],
    }
  }
}
在tsconfig.json中引入
{
  "extends": "./tsconfig.paths.json", //与compilerOptions同级
  "compilerOptions":{}
}
如果不想拆分配置,直接配置tsconfig.json,最终代码如下
{
  "compilerOptions": {
    "target": "es5", // 目标语言的版本
    "lib": ["dom", "dom.iterable", "esnext", "ScriptHost"],
    // 编译时引入的 ES 功能库,包括:es5 、es6、es7、dom 等。
    // 如果未设置,则默认为: target 为 es5 时: ["dom", "es5", "scripthost"]
    //target 为 es6 时: ["dom", "es6", "dom.iterable", "scripthost"]
    "allowJs": true, // 允许编译器编译JS,JSX文件
    "checkJs": true, //对JS文件进行检查
    "skipLibCheck": true,
    "esModuleInterop": true, // 允许export=导出,由import from 导入
    "allowSyntheticDefaultImports": true, //允许使用 import xxx from 'xxx' 的形式来引入模块
    "strict": true, // 开启所有严格的类型检查
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true, // 防止switch语句贯穿(即如果没有break语句后面不会执行)
    "module": "esnext", // 生成代码的模板标准
    "moduleResolution": "node", // 模块解析策略,ts默认用node的解析策略,即相对的方式导入
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true, // 不输出文件,即编译后不会生成任何js文件
    "jsx": "react-jsx",
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "less/*": ["./src/assets/styles/less/*"]
    }
  },
  "include": ["src", "**/*.ts", "**/*.tsx"],
  "exclude": ["./node_modules"]
}
重启项目,使配置生效
四、第三方库
当引用第三方插件在ts中报错时,先查找这第三方库是否有类型声明文件
网址:www.typescriptlang.org/dt/search?s…
或者鼠标移上去会有提示,让你先尝试安装某个包,不行再去声明模块
1. 有声明文件的库
示例:react-helmet库

查找结果:

进行安装
npm i @types/react-helmet --save-dev
2. 无声明文件的库
如果第三方js文件没有Typescript类型支持,需要通过声明文件解决
示例:react-i18next库
新建src/typings.d.ts(项目src目录下新建typings.d.ts文件)
//新增
declare module "react-i18next";
3. 注册在window上的库或变量
示例:注册在window上的库,例如echarts通过cdn引入
修改src/typings.d.ts
//新增
declare interface Window {
  echarts: any;
}
五、使用import引入非JS模块会报错
解决办法:给这些非JS模块添加声明
修改src/typings.d.ts
//新增
//style
declare module '*.css'
declare module '*.less'
declare module '*.scss'
//图片
declare module '*.svg'
declare module '*.png';
declare module '*.jpg';
六、将文件改为.tsx
先将.jsx文件改为.tsx文件,改完之后项目会编译报错,接下来继续解决报错的问题
1. 函数组件
React.FC 默认添加children?:React.ReactNode 属性声明
React.FC 自动校验 defaultProps 中字段
import {FC} from 'react'
interface Props{
  name?:string
}
const Greeting:FC<Props> =({name,children})=>{
  return <div>Hello,{name},{children}</div>
}
Greeting.defaultProps = {
  name:2
}
2. 类组件
<Props,State> 指定继承和 state 类型
interface Props {
  initialCount: number;
}
interface State {
  count: number;
}
class Counter extends React.Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = {
      count: props.initialCount,
    };
  }
}
3. 模块
如果有大量相同的接口Props,可提取到外部模块
新建src/types/echartsCompoent.ts
//echarts数据
interface ModelData {
  xAxis: string | number[];
  series: number[];
}
export interface Props {
  theme: string; //主题色
  model?: ModelData;
}
使用:
import React from 'react'
import { Props } from "@/types/echartsCompoent";
const Bar:React.FC<Props> = ({theme,model}) =>{return <></>}
export default React.memo(Bar)
4. 非空断言操作符 !
告诉编译器,对象为 非null 和 非undefined类型
示例:antd的BackTop组件的target属性,通过非空断言操作符告诉程序,这里的.current非空
const overFlowRef = useRef(null);
<BackTop visibilityHeight={100} target={() => overFlowRef.current!} />
5. React.createContext
这里是空对象,通过类型断言的方式,指定它的数据类型
import { Dispatch } from 'react';
interface UserContextType {
  phone: string,
  setPhone: Dispatch<React.SetStateAction<string>>
}
const UserContext = React.createContext<UserContextType>({} as UserContextType)
//子组件
const Button = (props: Props) => {
  const context = useContext(UserContext);
  return (
    <button onClick={() => context.setPhone('234')}>{props.name}---{context.phone}</button>
  )
}
//父组件
const App = () => {
  const [phone, setPhone] = useState('123')
  return (
    <UserContext.Provider value={{ phone, setPhone }}>
      <Button name="按钮" />
    </UserContext.Provider>
  );
}
6. 类型断言
情景:使用antd的Menu组件时,业务需要判断当前主题色,再决定是否高亮当前菜单栏
<Menu.Item
  key="theme-black"
  className={theme === "theme-black" && "themeActive"}
>
  夜间模式
</Menu.Item>
但是会报:不能将类型“false | "themeActive"”分配给类型“string | undefined”;这时候需手动告诉程序返回值的类型
<Menu.Item
  key="theme-black"
  className={(theme === "theme-black" && "themeActive") as string}
>
  夜间模式
</Menu.Item>
7. useRef
情景:ref上报错
方案:给useRef一个初始值null
const wrapper = useRef(null);
8. useState
- 指定初始值
指定初始值,ts可进行类型推论
const [currentPage, setCurrentPage] = useState(1);
拓展:如果设置useState([]),会被ts推导成never[]类型,因此需通过泛型指定初始值的类型
useState<any[]>([])
2. 不指定初始值
interface User{
  userName:string,
  userId:number,
}
function UserInfo() {
  const [userInfo, setUserInfo] = useState<User | undefined>();
}
3. 不指定初始值取值问题
当定义泛型包含undefined时,对象取值时会遇到“对象可能未定义”的校验错误
解决方法:使用可选链操作符 ?
interface User {
  userName: string;
  userId: number;
}
function UserInfo() {
  const [userInfo, setUserInfo] = useState<User | undefined>();
  return (
    <div>
      {userInfo?.userName}
    </div>
  );
}
9. useSelector
const { fileName } = useSelector((state) => state.file);
报错:类型“DefaultRootState“上不存在属性“xxx“
解决:
使用store.getState获取到所有redux中的数据
通过typeof反推出store的数据类型
通过ReturnType,获取函数类型的返回值类型
示例:
新建src/store/store.d.ts
import store from './index'
export type RootState = ReturnType<typeof store.getState>
使用
import { RootState } from '@/store/store'
const { fileName } = useSelector((state:RootState) => state.file);
10. useReducer
interface IncrementAction {
  type: "INCREMENT";
}
interface DecrementAction {
  type: "DECREMENT";
}
//类型别名
type CountAction = IncrementAction | DecrementAction;
function countReducer(state: number, action: CountAction) {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;
    case "DECREMENT":
      return state - 1;
    default:
      return state;
  }
}
function Counter() {
  const [count, dispatch] = useReducer(countReducer,0);
  return (
    <div>
      <button onClick={() => dispatch({type:'INCREMENT'})}>+</button>
      <div>{count}</div>
      <button onClick={() => dispatch({type:'DECREMENT'})}>-</button>
    </div>
  );
}
11. require.context
通过webpack获取文件内容时,报类型“NodeRequire”上不存在属性“context”。
方案:安装webpack的ts文件
npm i @types/webpack-env -D
七、React元素类型
| 情况 | 类别 | 
|---|---|
| 字符串、数字 | React.ReactText | 
| 单个jsx元素 | React.ReactElement | 
| 多个jsx元素 | React.ReactNode | 
| portal | React.ReactPortal | 
八、React事件类型
| 情况 | 类别 | 
|---|---|
| React.MouseEvent | 点击事件 | 
| React.keyboardEvent | 键盘事件 | 
| React.DragEvent | 拖拽事件 | 
| React.FocusEvent | 焦点事件 | 
| React.ChangeEvent | 表单域值变更事件 | 
| React.FormEvent | 表单提交事件 | 
| React.WheelEvent | 鼠标滚动事件 | 
| React.TouchEvent | 触摸事件 | 
1. 点击事件
interface Props{
  onClick?:(event:React.MouseEvent<HTMLButtonElement>)=>void
}
2. 表单事件
function App() {
  const [value1, setValue1] = useState('');
  const onChangeValue1 = (e:React.ChangeEvent<HTMLInputElement>)=>{
    setValue1(e.target.value)
  }
  return (
    <div>
      <input value={value1} onChange={onChangeValue1}/>
      <Button/>
    </div>
  );
}
欢迎关注:之后文章会首发在云在前端公众号,未经许可禁止转载!
转载自:https://juejin.cn/post/7052629990940082207




