如何优雅地在 React 中使用TypeScript,看这一篇就够了!(4)

简介: 毕业已有3月有余,工作用的技术栈主要是React hooks + TypeScript。其实在单独使用 TypeScript 时没有太多的坑,不过和React结合之后就会复杂很多。本文就来聊一聊TypeScript与React一起使用时经常遇到的一些类型定义的问题。阅读本文前,希望你能有一定的React和TypeScript基础

六、工具泛型


在项目中使用一些工具泛型可以提高我们的开发效率,少写很多类型定义。下面来看看有哪些常见的工具泛型,以及其使用方式。


1. Partial


Partial 作用是将传入的属性变为可选项。适用于对类型结构不明确的情况。它使用了两个关键字:keyof和in,先来看看他们都是什么含义。keyof 可以用来取得接口的所有 key 值:

interface IPerson {
  name: string;
  age: number;
  height: number;
}
type T = keyof IPerson 
// T 类型为: "name" | "age" | "number"
复制代码


in关键字可以遍历枚举类型,:

type Person = "name" | "age" | "number"
type Obj =  {
  [p in Keys]: any
} 
// Obj类型为: { name: any, age: any, number: any }
复制代码


keyof 可以产生联合类型, in 可以遍历枚举类型, 所以经常一起使用, 下面是Partial工具泛型的定义:

/**
 * Make all properties in T optional
 * 将T中的所有属性设置为可选
 */
type Partial<T> = {
    [P in keyof T]?: T[P];
};
复制代码


这里,keyof T 获取 T 所有属性名, 然后使用 in 进行遍历, 将值赋给 P, 最后 T[P] 取得相应属性的值。中间的?就用来将属性设置为可选。

使用示例如下:

interface IPerson {
  name: string;
  age: number;
  height: number;
}
const person: Partial<IPerson> = {
  name: "zhangsan";
}
复制代码


2. Required


Required 的作用是将传入的属性变为必选项,和上面的工具泛型恰好相反,其声明如下:

/**
 * Make all properties in T required
 * 将T中的所有属性设置为必选
 */
type Required<T> = {
    [P in keyof T]-?: T[P];
};
复制代码


可以看到,这里使用-?将属性设置为必选,可以理解为减去问号。适用形式和上面的Partial差不多:

interface IPerson {
  name?: string;
  age?: number;
  height?: number;
}
const person: Required<IPerson> = {
  name: "zhangsan";
  age: 18;
  height: 180;
}
复制代码


3. Readonly


将T类型的所有属性设置为只读(readonly),构造出来类型的属性不能被再次赋值。Readonly的声明形式如下:

/**
 * Make all properties in T readonly
 */
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};
复制代码


使用示例如下:

interface IPerson {
  name: string;
  age: number;
}
const person: Readonly<IPerson> = {
  name: "zhangsan",
  age: 18
}
person.age = 20;  //  Error: cannot reassign a readonly property
复制代码


可以看到,通过 Readonly 将IPerson的属性转化成了只读,不能再进行赋值操作。


4. Pick<T, K extends keyof T>


从T类型中挑选部分属性K来构造新的类型。它的声明形式如下:

/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};
复制代码


使用示例如下:

interface IPerson {
  name: string;
  age: number;
  height: number;
}
const person: Pick<IPerson, "name" | "age"> = {
  name: "zhangsan",
  age: 18
}
复制代码


5. Record<K extends keyof any, T>


Record 用来构造一个类型,其属性名的类型为K,属性值的类型为T。这个工具泛型可用来将某个类型的属性映射到另一个类型上,下面是其声明形式:

/**
 * Construct a type with a set of properties K of type T
 */
type Record<K extends keyof any, T> = {
    [P in K]: T;
};
复制代码


使用示例如下:

interface IPageinfo {
    title: string;
}
type IPage = 'home' | 'about' | 'contact';
const page: Record<IPage, IPageinfo> = {
    about: {title: 'about'},
    contact: {title: 'contact'},
    home: {title: 'home'},
}
复制代码


6. Exclude<T, U>


Exclude 就是从一个联合类型中排除掉属于另一个联合类型的子集,下面是其声明的形式:

/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T;
复制代码


使用示例如下:

interface IPerson {
  name: string;
  age: number;
  height: number;
}
const person: Exclude<IPerson, "age" | "sex"> = {
  name: "zhangsan";
  height: 180;
}
复制代码


7. Omit<T, K extends keyof any>


上面的Pick 和 Exclude 都是最基础基础的工具泛型,很多时候用 Pick 或者 Exclude 还不如直接写类型更直接。而 Omit 就基于这两个来做的一个更抽象的封装,它允许从一个对象中剔除若干个属性,剩下的就是需要的新类型。下面是它的声明形式:

/**
 * Construct a type with the properties of T except for those in type K.
 */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
复制代码


使用示例如下:

interface IPerson {
  name: string;
  age: number;
  height: number;
}
const person: Omit<IPerson, "age" | "height"> = {
  name: "zhangsan";
}
复制代码


8. ReturnType


ReturnType会返回函数返回值的类型,其声明形式如下:

/**
 * Obtain the return type of a function type
 */
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
复制代码


使用示例如下:

function foo(type): boolean {
  return type === 0
}
type FooType = ReturnType<typeof foo>
复制代码


这里使用 typeof 是为了获取 foo 的函数签名,等价于 (type: any) => boolean。


七、Axios 封装


在React项目中,我们经常使用Axios库进行数据请求,Axios 是基于 Promise 的 HTTP 库,可以在浏览器和 node.js 中使用。Axios 具备以下特性:

  • 从浏览器中创建 XMLHttpRequests;
  • 从 node.js 创建 HTTP 请求;
  • 支持 Promise API;
  • 拦截请求和响应;
  • 转换请求数据和响应数据;
  • 取消请求;
  • 自动转换 JSON 数据;
  • 客户端支持防御 XSRF。

Axios的基本使用就不再多介绍了。为了更好地调用,做一些全局的拦截,通常会对Axios进行封装,下面就使用TypeScript对Axios进行简单封装,使其同时能够有很好的类型支持。Axios是自带声明文件的,所以我们无需额外的操作。


下面来看基本的封装:

import axios, { AxiosInstance, AxiosRequestConfig, AxiosPromise,AxiosResponse } from 'axios'; // 引入axios和定义在node_modules/axios/index.ts文件里的类型声明
 // 定义接口请求类,用于创建axios请求实例
class HttpRequest {
  // 接收接口请求的基本路径
  constructor(public baseUrl: string) { 
    this.baseUrl = baseUrl;
  }
  // 调用接口时调用实例的这个方法,返回AxiosPromise
  public request(options: AxiosRequestConfig): AxiosPromise { 
    // 创建axios实例,它是函数,同时这个函数包含多个属性
    const instance: AxiosInstance = axios.create() 
    // 合并基础路径和每个接口单独传入的配置,比如url、参数等
    options = this.mergeConfig(options) 
    // 调用interceptors方法使拦截器生效
    this.interceptors(instance, options.url) 
    // 返回AxiosPromise
    return instance(options) 
  }
  // 用于添加全局请求和响应拦截
  private interceptors(instance: AxiosInstance, url?: string) { 
    // 请求和响应拦截
  }
  // 用于合并基础路径配置和接口单独配置
  private mergeConfig(options: AxiosRequestConfig): AxiosRequestConfig { 
    return Object.assign({ baseURL: this.baseUrl }, options);
  }
}
export default HttpRequest;
复制代码


通常baseUrl在开发环境的和生产环境的路径是不一样的,所以可以根据当前是开发环境还是生产环境做判断,应用不同的基础路径。这里要写在一个配置文件里:

export default {
    api: {
        devApiBaseUrl: '/test/api/xxx',
        proApiBaseUrl: '/api/xxx',
    },
};
复制代码


在上面的文件中引入这个配置:

import { api: { devApiBaseUrl, proApiBaseUrl } } from '@/config';
const apiBaseUrl = env.NODE_ENV === 'production' ? proApiBaseUrl : devApiBaseUrl;
复制代码


之后就可以将apiBaseUrl作为默认值传入HttpRequest的参数:

class HttpRequest { 
  constructor(public baseUrl: string = apiBaseUrl) { 
    this.baseUrl = baseUrl;
  }
复制代码


接下来可以完善一下拦截器类,在类中interceptors方法内添加请求拦截器和响应拦截器,实现对所有接口请求的统一处理:

private interceptors(instance: AxiosInstance, url?: string) {
    // 请求拦截
    instance.interceptors.request.use((config: AxiosRequestConfig) => {
      // 接口请求的所有配置,可以在axios.defaults修改配置
      return config
    },
    (error) => {
      return Promise.reject(error)
    })
    // 响应拦截
    instance.interceptors.response.use((res: AxiosResponse) => {
      const { data } = res 
      const { code, msg } = data
      if (code !== 0) {
        console.error(msg) 
      }
      return res
    },
    (error) => { 
      return Promise.reject(error)
    })
  }
复制代码


到这里封装的就差不多了,一般服务端会将状态码、提示信息和数据封装在一起,然后作为数据返回,所以所有请求返回的数据格式都是一样的,所以就可以定义一个接口来指定返回的数据结构,可以定义一个接口:

export interface ResponseData {
  code: number
  data?: any
  msg: string
}
复制代码


接下来看看使用TypeScript封装的Axios该如何使用。可以先定义一个请求实例:

import HttpRequest from '@/utils/axios'
export * from '@/utils/axios'
export default new HttpRequest()
复制代码


这里把请求类导入进来,默认导出这个类的实例。之后创建一个登陆接口请求方法:

import axios, { ResponseData } from './index'
import { AxiosPromise } from 'axios'
interface ILogin {
  user: string;
  password: number | string
}
export const loginReq = (data: ILogin): AxiosPromise<ResponseData> => {
  return axios.request({
    url: '/api/user/login',
    data,
    method: 'POST'
  })
}
复制代码


这里封装登录请求方法loginReq,他的参数必须是我们定义的ILogin接口的类型。这个方法返回一个类型为AxiosPromise的Promise,AxiosPromise是axios声明文件内置的类型,可以传入一个泛型变量参数,用于指定返回的结果中data字段的类型。


接下来可以调用一下这个登录的接口:

import { loginReq } from '@/api/user'
const Home: FC = () => {
  const login = (params) => {
    loginReq(params).then((res) => {
      console.log(res.data.code)
    })  
  }  
}
复制代码


通过这种方式,当我们调用loginReq接口时,就会提示我们,参数的类型是ILogin,需要传入几个参数。这样编写代码的体验就会好很多。


八. 其他


1. import React

在React项目中使用TypeScript时,普通组件文件后缀为.tsx,公共方法文件后缀为.ts。在. tsx 文件中导入 React 的方式如下:

import * as React from 'react'
import * as ReactDOM from 'react-dom'
复制代码


这是一种面向未来的导入方式,如果想在项目中使用以下导入方式:

import React from "react";
import ReactDOM from "react-dom";
复制代码


就需要在tsconfig.json配置文件中进行如下配置:

"compilerOptions": {
    // 允许默认从没有默认导出的模块导入。
    "allowSyntheticDefaultImports": true,
}
复制代码


2. Types or Interfaces?


我们可以使用types或者Interfaces来定义类型吗,那么该如何选择他俩呢?建议如下:

  • 在定义公共 API 时(比如编辑一个库)使用 interface,这样可以方便使用者继承接口,这样允许使用最通过声明合并来扩展它们;
  • 在定义组件属性(Props)和状态(State)时,建议使用 type,因为 type 的约束性更强。


interface 和 type 在 ts 中是两个不同的概念,但在 React 大部分使用的 case 中,interface 和 type 可以达到相同的功能效果,type 和 interface 最大的区别是:type 类型不能二次编辑,而 interface 可以随时扩展:

interface Animal {
  name: string
}
// 可以继续在原属性基础上,添加新属性:color
interface Animal {
  color: string
}
type Animal = {
  name: string
}
// type类型不支持属性扩展
// Error: Duplicate identifier 'Animal'
type Animal = {
  color: string
}
复制代码


type对于联合类型是很有用的,比如:type Type = TypeA | TypeB。而interface更适合声明字典类行,然后定义或者扩展它。


3. 懒加载类型


如果我们想在React router中使用懒加载,React也为我们提供了懒加载方法的类型,来看下面的例子:

export interface RouteType {
    pathname: string;
    component: LazyExoticComponent<any>;
    exact: boolean;
    title?: string;
    icon?: string;
    children?: RouteType[];
}
export const AppRoutes: RouteType[] = [
    {
        pathname: '/login',
        component: lazy(() => import('../views/Login/Login')),
        exact: true
    },
    {
        pathname: '/404',
        component: lazy(() => import('../views/404/404')),
        exact: true,
    },
    {
        pathname: '/',
        exact: false,
        component: lazy(() => import('../views/Admin/Admin'))
    }
]
复制代码


下面是懒加载类型和lazy方法在声明文件中的定义:

type LazyExoticComponent<T extends ComponentType<any>> = ExoticComponent<ComponentPropsWithRef<T>> & {
  readonly _result: T;
};
function lazy<T extends ComponentType<any>>(
factory: () => Promise<{ default: T }>
): LazyExoticComponent<T>;
复制代码


4. 类型断言


类型断言(Type Assertion)可以用来手动指定一个值的类型。在React项目中,断言还是很有用的,。有时候推断出来的类型并不是真正的类型,很多时候我们可能会比TS更懂我们的代码,所以可以使用断言(使用as关键字)来定义一个值得类型。


来看下面的例子:

const getLength = (target: string | number): number => {
  if (target.length) { // error 类型"string | number"上不存在属性"length"
    return target.length; // error  类型"number"上不存在属性"length"
  } else {
    return target.toString().length;
  }
};
复制代码


当TypeScript不确定一个联合类型的变量到底是哪个类型时,就只能访问此联合类型的所有类型里共有的属性或方法,所以现在加了对参数target和返回值的类型定义之后就会报错。这时就可以使用断言,将target的类型断言成string类型:

const getStrLength = (target: string | number): number => {
  if ((target as string).length) {      
    return (target as string).length; 
  } else {
    return target.toString().length;
  }
};
复制代码


需要注意,类型断言并不是类型转换,断言成一个联合类型中不存在的类型是不允许的。

再来看一个例子,在调用一个方法时传入参数:

网络异常,图片无法展示
|


这里就提示我们这个参数可能是undefined,而通过业务知道这个值是一定存在的,所以就可以将它断言成数字:data?.subjectId as number

除此之外,上面所说的标签类型、组件类型、时间类型都可以使用断言来指定给一些数据,还是要根据实际的业务场景来使用。

感悟:使用类型断言真的能解决项目中的很多报错~


5. 枚举类型


枚举类型在项目中的作用也是不可忽视的,使用枚举类型可以让代码的扩展性更好,当我想更改某属性值时,无需去全局更改这个属性,只要更改枚举中的值即可。通常情况下,最好新建一个文件专门来定义枚举值,便于引用。

相关文章
|
5月前
|
前端开发 JavaScript 安全
TypeScript在React Hooks中的应用:提升React开发的类型安全与可维护性
【7月更文挑战第17天】TypeScript在React Hooks中的应用极大地提升了React应用的类型安全性和可维护性。通过为状态、依赖项和自定义Hooks指定明确的类型,开发者可以编写更加健壮、易于理解和维护的代码。随着React和TypeScript的不断发展,结合两者的优势将成为构建现代Web应用的标准做法。
|
1月前
|
前端开发 JavaScript
手敲Webpack 5:React + TypeScript项目脚手架搭建实践
手敲Webpack 5:React + TypeScript项目脚手架搭建实践
|
2月前
|
JavaScript 前端开发 安全
使用 TypeScript 加强 React 组件的类型安全
【10月更文挑战第1天】使用 TypeScript 加强 React 组件的类型安全
39 3
|
4月前
|
JavaScript 前端开发 安全
[译] 使用 TypeScript 开发 React Hooks
[译] 使用 TypeScript 开发 React Hooks
|
4月前
|
开发者 自然语言处理 存储
语言不再是壁垒:掌握 JSF 国际化技巧,轻松构建多语言支持的 Web 应用
【8月更文挑战第31天】JavaServer Faces (JSF) 框架提供了强大的国际化 (I18N) 和本地化 (L10N) 支持,使开发者能轻松添加多语言功能。本文通过具体案例展示如何在 JSF 应用中实现多语言支持,包括创建项目、配置语言资源文件 (`messages_xx.properties`)、设置 `web.xml`、编写 Managed Bean (`LanguageBean`) 处理语言选择,以及使用 Facelets 页面 (`index.xhtml`) 显示多语言消息。通过这些步骤,你将学会如何配置 JSF 环境、编写语言资源文件,并实现动态语言切换。
42 0
|
4月前
|
前端开发 JavaScript 安全
【前端开发新境界】React TypeScript融合之路:从零起步构建类型安全的React应用,全面提升代码质量和开发效率的实战指南!
【8月更文挑战第31天】《React TypeScript融合之路:类型安全的React应用开发》是一篇详细教程,介绍如何结合TypeScript提升React应用的可读性和健壮性。从环境搭建、基础语法到类型化组件、状态管理及Hooks使用,逐步展示TypeScript在复杂前端项目中的优势。适合各水平开发者学习,助力构建高质量应用。
65 0
|
5月前
|
JavaScript 前端开发 IDE
React 项目中有效地使用 TypeScript
React 项目中有效地使用 TypeScript
|
Web App开发 JavaScript 数据格式
一次解决React+TypeScript+Webpack 别名(alias)找不到问题的过程
一次解决React+TypeScript+Webpack 别名(alias)找不到问题的过程
15302 0
|
7月前
|
设计模式 前端开发 数据可视化
【第4期】一文了解React UI 组件库
【第4期】一文了解React UI 组件库
366 0
|
7月前
|
资源调度 前端开发 JavaScript
React 的antd-mobile 组件库,嵌套路由
React 的antd-mobile 组件库,嵌套路由
122 0