likes
comments
collection
share

React老项目迁移Typescript(持续更新ing)

作者站长头像
站长
· 阅读数 64

前言

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库

React老项目迁移Typescript(持续更新ing)

查找结果:

React老项目迁移Typescript(持续更新ing)

进行安装

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

  1. 指定初始值

指定初始值,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
portalReact.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
评论
请登录