前端小魔女

V1

2022/06/23阅读:22主题:蔷薇紫

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,以帮助建立健壮的组件。

泛型 GenericsTS中的一个重要部分,这篇文章就来简单介绍一下其概念并在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 <TU>(value: T, message: U) : T {
 console.log(message);
 return value;
}
console.log(identity<Numberstring>(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)
): [SDispatch<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'🍌' },
  { value'苹果'label'🍎' },
  { value'椰子'label'🥥' },
  { value'西瓜'label'🍉' },
];

function Form({
  return <Select options={mockOptions} />;
}

假设,对于select的选项的value,我们可以接受字符串或数字但不能同时接受两者

我们尝试下面的代码。

type Option = {
  value: number | string;
  label: string;
};

type SelectProps = {
  options: Option[];
};

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

上面代码不满足我们的情况。原因是,在一个select数组中,你可能有一个select的值是数字类型,而另一个select的值是字符串类型。我们不希望这样,但 TypeScript 会接受它。

例如存在如下的数据。


const mockOptions = [
  { value123label'🍌' }, // 数字类型
  { value'苹果'label'🍎' }, // 字符串类型
  { value'椰子'label'🥥' },
  { value'西瓜'label'🍉' },
];

而我们可以通过泛型来强制使组件接收到的select值要么是数字类型,要么是字符串类型

type OptionValue = number | string;

// 泛型约束
type Option<Type extends OptionValue> = {
  value: Type;
  label: string;
};

type SelectProps<Type extends OptionValue> = {
  options: Option<Type>[];
};

组件定义

function Select<Type extends OptionValue>({ options }: SelectProps<Type>{
  const [value, setValue] = useState<Type>(options[0]?.value);

  return (
   ....
  );
}

为什么我们要定义 OptionValue ,然后在很多地方加上extends OptionValue

想象一下,我们不这样做,而只是用Type extends OptionValue来代替Typeselect组件怎么会知道 Type 可以是一个数字或一个字符串,而不是其他?


后记

分享是一种态度

参考资料:

全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。

分类:

前端

标签:

React.js

作者介绍

前端小魔女
V1