当我们认为不可能是可能的时候,那么不可能就会变成可能,就会真的发生 -- 皮格马利翁效应
大家好,我是柒八九。
今天,{又双叒叕| yòu shuāng ruò zhuó}开辟了一个新的领域--TypeScript实战系列。
这是继
这些模块,又新增的知识体系。
该系列的主要是针对React + TS
的。而关于TS
的种种优点和好处,就不在赘述了,已经被说烂了。
last but not least,此系列文章是TS + React
的应用文章,针对一些比较基础的例如TS
的各种数据类型,就不做过多的介绍。网上有很多文章。
时不我待,我们开始。
你能所学到的知识点
TypeScript
简单概念- {泛型| Generics}的概念和使用方式
- 在
React
利用泛型定义hook
和props
文章概要
TypeScript
是什么- {泛型| Generics} 是个啥
- 在React中使用泛型
1. TypeScript
是什么
TypeScript
是⼀种由微软开源的编程语⾔。它是JavaScript
的⼀个超集,本质上向JS
添加了可选的静态类型和基于类的⾯向对象编程。
TypeScript
提供最新的和不断发展的 JavaScript
特性,包括那些来⾃ 2015 年的 ECMAScript 和未来的提案中的特性,⽐如异步功能和 Decorators
,以帮助建⽴健壮的组件。
关于ES
和JS
直接的关系,
在浏览器环境下,
JS = ECMAScript + DOM + BOM
。
想详细了解可以参考之前的文章,我们这里就不过多区分,ES
和JS
的关系了。
TypeScript
与 JavaScript
的区别
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.ts
、b.ts
和 c.ts
。这些⽂件将被 TypeScript
编译器,根据配置的编译选项编译成 3 个 js ⽂件,即 a.js
、b.js
和 c.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
函数做适当的调整,以⽀持 TypeScript
的 Number
类型的参数:
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 复制代码
看到 语法,就像传递参数⼀样,上面代码传递了我们想要⽤于特定函数调⽤的类型。
参考上⾯的图⽚,当我们调⽤ identity(1)
, Number
类型就像参数 1 ⼀样,它将在出现 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>>]; 复制代码
我们抽丝剥茧的来分析一下这个类型的定义。
- 首先定义了一个函数(
useState
)它接受一个叫做S
的泛型变量 - 这个函数接受一个也是唯一的一个参数:
initialState
(初始状态)
- 这个初始状态可以是一个类型为
S
(传入泛型)的变量,也可以是一个返回类型为S
的函数
useState
返回一个有两个元素的数组
- 第一个是
S类型
的值(state
值) - 第二个是
Dispatch类型
,其泛型参数为SetStateAction
。
而SetStateAction
本身又接收了类型为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 = [ { value: 123, label: '🍌' }, // 数字类型 { 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
来代替Type
。select
组件怎么会知道 Type
可以是一个数字或一个字符串,而不是其他?
后记
分享是一种态度。
参考资料:
React_Ts_泛型重写TSTS官网
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。