TS_React:使用泛型来改善类型

大家好,我是「柒八九」

今天,又双叒叕yòu shuāng ruò zhuó开辟了一个新的领域--「TypeScript实战系列」

这是继

  1. JS基础&原理
  2. JS算法
  3. 前端工程化
  4. 浏览器知识体系
  5. Css
  6. 网络通信
  7. 前端框架

这些模块,又新增的知识体系。

该系列的主要是针对React + TS的。而关于TS的种种优点和好处,就不再赘述了,已经被说烂了。

「last but not least」,此系列文章TS + React的应用文章,针对一些比较基础的例如TS的各种数据类型,就不做过多的介绍。网上有很多文章

时不我待,我们开始。

你能所学到的知识点

  1. TypeScript简单概念
  2. 泛型Generics的概念和使用方式
  3. React利用泛型定义hookprops

文章概要

  1. TypeScript 是什么
  2. 泛型Generics 是个啥
  3. 在React中使用泛型

1. TypeScript 是什么

TypeScript 是⼀种由微软开源的编程语⾔。它是 JavaScript 的⼀个「超集」,本质上向JS添加了可选的「静态类型」「基于类的⾯向对象编程」。 ❞

TypeScript 提供最新的和不断发展的 JavaScript 特性,包括那些来⾃ 2015 年的 ECMAScript 和未来的提案中的特性,⽐如异步功能Decorators,以帮助建⽴健壮的组件。

关于ESJS间的关系,

❝在「浏览器环境下」JS = ECMAScript + DOM + BOM

想详细了解可以参考之前的文章,我们这里就不过多区分,ESJS的关系了。


TypeScriptJavaScript 的区别

TypeScript

JavaScript

JavaScript 的「超集」⽤于解决⼤型项⽬的代码复杂性

⼀种「脚本语⾔」⽤于创建动态⽹⻚

可以在「编译期间」发现并纠正错误

作为⼀种「解释型语⾔」,「只能」在运⾏时发现错误

「强类型」,⽀持静态和动态类型

「弱类型」,没有静态类型选项

最终被编译成 JavaScript 代码,使浏览器可以理解

可以直接在浏览器中使⽤

⽀持模块、泛型和接⼝

不⽀持泛型或接⼝


获取 TypeScript

命令⾏的 TypeScript 编译器可以使⽤ npm 包管理器来安装。

安装 TypeScript

$ npm install -g typescript

验证 TypeScript

$ tsc -v
Version 4.9.x  // TS最新版本

编译 TypeScript ⽂件

$ tsc helloworld.ts
helloworld.ts => helloworld.js

典型 TypeScript ⼯作流程

在上图中包含 3 个 ts ⽂件:a.tsb.tsc.ts。这些⽂件将被 TypeScript 编译器,根据配置的编译选项编译成 3 个 js ⽂件,即 a.jsb.jsc.js。对于⼤多数使⽤ TypeScript 开发的 Web 项⽬,我们还会对编译⽣成的 js ⽂件进⾏「打包处理」,然后进⾏部署。

TypeScript的特点

TypeScript 主要有 3 大特点:

  • 「始于JavaScript,归于JavaScript」 TypeScript 可以编译出纯净、 简洁的 JavaScript 代码,并且可以运行在任何浏览器上、Node.js 环境中和任何支持 ECMAScript 3(或更高版本)的JavaScript 引擎中。
  • 「强大的类型系统」 「类型系统」允许 JavaScript 开发者在开发 JavaScript 应用程序时使用高效的开发工具和常用操作比如静态检查和代码重构。
  • 「先进的 JavaScript」 TypeScript 提供最新的和不断发展的 JavaScript 特性,包括那些来自 2015 年的 ECMAScript 和未来的提案中的特性,比如异步功能和 Decorators,以帮助建立健壮的组件。

泛型Generics 是TS中的一个重要部分,这篇文章就来简单介绍一下其概念并在React中的应用。

1. 泛型Generics 是个啥?

❝泛型指的是「类型参数化」:即将原来某种具体的类型进⾏参数化 ❞

软件⼯程中,我们不仅要创建⼀致的、定义良好的 API,同时也要考虑「可重⽤性」。组件不仅能够⽀持当前的数据类型,同时也能⽀持未来的数据类型,这在创建⼤型系统时为你提供了⼗分灵活的功能

在像 C++/Java/Rust 这样的传统 OOP 语⾔中,可以「使⽤泛型来创建可重⽤的组件,⼀个组件可以⽀持多种类型的数据」。这样⽤户就可以以⾃⼰的数据类型来使⽤组件

❝设计泛型的「关键⽬的」是在「成员之间提供有意义的约束」,这些成员可以是:类的实例成员、类的⽅法、函数参数和函数返回值。 ❞

举个例子,将标准的 TypeScript类型JavaScript对象进行比较。

//  JavaScript 对象
const user = {
  name: '789',
  status: '在线',
};

// TypeScript 类型
type User = {
  name: string;
  status: string;
};

正如你所看到的,它们非常相像。

❝主要的「区别」

  • JavaScript 中,关心的是变量的「值」
  • TypeScript 中,关心的是变量的「类型」

关于我们的User类型,它的状态属性模糊了。一个状态通常有「预定义的值」,比方说在这个例子中它可以是 在线离线

type User = {
  name: string;
  status: '在线' | '离线';
};

上面的代码是假设我们「已经知道有哪种状态」了。如果我们不知道,而状态信息可能会根据实际情况发生变化?这就需要泛型来处理这种情况:「它可以让你指定一个可以根据使用情况而改变的类型」

但对于我们的User例子来说,使用一个「泛型」看起来是这样的。

// `User` 现在是泛型类型
const user: User<'在线' | '离线'>;

// 我们可以手动新增一个新的类型 (空闲)
const user: User<'在线' | '离线' | '空闲'>;

上面说的是 user变量是类型为User的对象。

我们继续来实现这个「类型」

// 定义一个泛型类型
type User<StatusOptions> = {
  name: string;
  status: StatusOptions;
};

StatusOptions 被称为 类型变量type variable,而 User 被说成是 泛型类型generic type。

上面的例子中,我们使用了<>来定义泛型。我们也可以使用函数来定义泛型。

type User = (StatusOption) => {
  return {
    name: string;
    status: StatusOptions;
  }
}

例如,设想我们的User接受了一个状态数组,而不是像以前那样接受一个单一的状态。这仍然很容易用一个泛型来做。

// 定义类型
type User<StatusOptions> = {
  name: string;
  status: StatusOptions[];
};

//类型的使用方式还是不变
const user: User<'在线' | '离线'>;

泛型有啥用?

上面的例子可以定义一个Status类型,然后用它来代替泛型。

type Status = '在线' | '离线';

type User = {
  name: string;
  status: Status;
};

这个处理方式在简单点的例子中是这样,但有很多情况下不能这样做。通常的情况是,当你想让一个类型在多个实例中共享,而每个实例都有一些不同」:即这个类型是「动态」的。

⾸先我们来定义⼀个通⽤的 identity 函数函数「返回值的类型」与它的「参数相同」

function identity (value) {
 return value;
}
console.log(identity(1)) // 1

现在,将 identity 函数做适当的调整,以⽀持 TypeScriptNumber 类型的参数:

function identity (value: Number) : Number {
 return value;
}
console.log(identity(1)) // 1

对于 identity函数 我们将 Number 类型分配给参数返回类型,使该函数「仅可⽤于该原始类型」。但该函数并不是可扩展或通⽤的

可以把 Number 换成 any ,这样就失去了定义应该返回哪种类型的能⼒,并且在这个过程中使「编译器失去了类型保护的作⽤」。我们的⽬标是让 identity 函数可以适⽤于「任何特定的类型」,为了实现这个⽬标,我们可以使⽤「泛型」解决这个问题,具体实现⽅式如下:

function identity <T>(value: T) : T {
 return value;
}
console.log(identity<Number>(1)) // 1

看到 <T> 语法,就「像传递参数⼀样」,上面代码传递了我们想要⽤于特定函数调⽤的类型。

参考上⾯的图⽚,当我们调⽤ identity<Number>(1)Number 类型就像参数 1 ⼀样,它将「在出现 T 的任何位置填充该类型」。图中 <T> 内部的 T 被称为「类型变量」,它是我们希望传递给 identity 函数「类型占位符」,同时它被分配给 value 参数⽤来代替它的类型:此时 T 充当的是类型,⽽不是特定的 Number 类型。

其中 T 代表 Type,在定义泛型时通常⽤作第⼀个类型变量名称。但实际上 T 可以⽤任何有效名称代替。除了 T 之外,以下是常⻅泛型变量代表的意思:

  • K(Key):表示对象中的键类型
  • V(Value):表示对象中的值类型
  • E(Element):表示元素类型

也可以引⼊希望定义的「任何数量的类型变量」。⽐如我们引⼊⼀个新的类型变量 U ,⽤于扩展我们定义的 identity 函数

function identity <T, U>(value: T, message: U) : T {
 console.log(message);
 return value;
}
console.log(identity<Number, string>(68, "TS真的香喷喷"));

泛型约束

有时我们可能希望「限制每个类型变量接受的类型数量,这就是「泛型约束」的作⽤。下⾯我们来举⼏个例⼦,介绍⼀下如何使⽤泛型约束。

确保属性存在

有时候,我们希望「类型变量对应的类型上存在某些属性。这时,除⾮我们显式地将特定属性定义为类型变量,否则编译器不会知道它们的存在。

例如在处理字符串或数组时,我们会假设 length 属性是可⽤的。让我们再次使⽤ identity 函数并尝试输出参数的⻓度:

function identity<T>(arg: T): T {
 console.log(arg.length); // Error
 return arg;
}

在这种情况下,「编译器」将不会知道 T 确实含有 length 属性,尤其是在可以「将任何类型赋给类型变量 T 的情况下」。我们需要做的就是让类型变量 extends ⼀个含有我们所需属性的接⼝,⽐如这样:

interface Length {
 length: number;
}
function identity<T extends Length>(arg: T): T {
 console.log(arg.length); // 可以获取length属性
 return arg;
}

T extends Length ⽤于告诉编译器,我们⽀持已经实现 Length 接⼝的任何类型。

箭头函数在jsx中的泛型语法

在前面的例子中,我们只举例了如何用泛型定义常规的函数语法,而不是ES6中引入的箭头函数语法。

// ES6的箭头函数语法
const identity = (arg) => {
  return arg;
};

原因是在使用JSX时,TypeScript 对箭头函数的处理并不像普通函数那样好。按照上面 TS处理函数的情况,写了如下的代码

// 不起作用
const identity<ArgType> = (arg: ArgType): ArgType => {
  return arg;
}

// 不起作用
const identity = <ArgType>(arg: ArgType): ArgType => {
  return arg;
}

上面两个例子,在使用JSX时,都不起作用。如果想要在处理箭头函数,需要使用下面的语法。

// 方式1
const identity = <ArgType,>(arg: ArgType): ArgType => {
  return arg;
};

// 方式2
const identity = <ArgType extends unkNown>(arg: ArgType): ArgType => {
  return arg;
};

出现上述问题的根源在于:「这是TSX(TypeScript+ JSX)的特定语法」。在正常的 TypeScript 中,不需要使用这种变通方法


泛型示例:useState

先让我们来看看 useState函数类型定义。

function useState<S>(
  initialState: S | (() => S)
): [S, dispatch<SetStateAction<S>>];

我们抽丝剥茧的来分析一下这个类型的定义。

  1. 首先定义了一个函数useState)它接受一个叫做S的泛型变量
  2. 这个函数接受一个也是唯一的一个参数:initialState(初始状态)
    • 这个初始状态可以是一个类型为 S(传入泛型)的变量,也可以是一个返回类型为S函数
  3. useState 返回一个有两个元素的数组
    • 一个S类型的值(state值)
    • 第二个是dispatch类型,其泛型参数为SetStateAction<S>。 而SetStateAction<S>本身又接收了类型为S的参数。

首先,我们来看看 SetStateAction

type SetStateAction<S> = S | ((prevstate: S) => S);

SetStateAction 也是一个泛型,它接收的变量既可以是一个S类型的变量,也可以是一个将S作为其参数类型和返回类型的函数

这让我想起了我们利用 setState 定义 state

  • 可以「直接提供新的状态值」
  • 或者提供一个函数,从旧的状态值上建立新的状态值。

然后,我们再继续看看dispatch发生了啥?

type dispatch<A> = (value: A) => void;

dispatch一个接收泛型参数A,并且不会返回任何值的函数

把它们拼接到一起,就是如下的代码

// 原始类型
type dispatch<SetStateAction<S>>
// 合并后
type (value: S | ((prevstate: S) => S)) => void

它是一个接受一个S一个函数S => S,并且不返回任何东西的函数

3. 在React中使用泛型

现在我们已经理解了泛型的概念,我们可以看看如何在React代码中应用它。

利用泛型处理Hook

Hook只是普通的JavaScript函数,只不过在React中有点额外调用时机和规则。由此可见,在Hook上使用泛型和在普通的 JavaScript 函数上使用是一样的。 ❞

//普通js函数
const greeting = identity<string>('Hello World');

// useState 
const [greeting, setGreeting] = useState<string>('Hello World');

在上面的例子中,你可以省略显式泛型,因为 TypeScript 可以从参数值中推断出它。但有时 TypeScript 不能这样做(或做错了),这就是要使用的语法。

我们只是针对useState一类hook进行分析,我们后期还有对其他hook做一个TS相关的分析处理。

利用泛型处理组件props

假设,你正在为一个表单构建一个select组件。代码如下:

「组件定义」

import { useState, ChangeEvent } from 'react';

function Select({ options }) {
  const [value, setValue] = useState(options[0]?.value);

  function handleChange(event: ChangeEvent<HTMLSelectElement>) {
    setValue(event.target.value);
  }

  return (
    <select value={value} onChange={handleChange}>
      {options.map((option) => (
        <option key={option.value} value={option.value}>
          {option.label}
        </option>
      ))}
    </select>
  );
}

export default Select;

「组件调用


// label 选项
const mockOptions = [
  { value: '香蕉', label: '


                  
                

相关文章

一、前言 在组件方面react和Vue一样的,核心思想玩的就是组件...
前言: 前段时间学习完react后,刚好就接到公司一个react项目...
前言: 最近收到组长通知我们项目组后面新开的项目准备统一技...
react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom...