大家好,我是「柒八九」。
今天,又双叒叕yòu shuāng ruò zhuó开辟了一个新的领域--「TypeScript实战系列」。
这是继
这些模块,又新增的知识体系。
该系列的主要是针对React + TS
的。而关于TS
的种种优点和好处,就不再赘述了,已经被说烂了。
「last but not least」,此系列文章是TS + React
的应用文章,针对一些比较基础的例如TS
的各种数据类型,就不做过多的介绍。网上有很多文章。
时不我待,我们开始。
❝
TypeScript
简单概念React
利用泛型定义hook
和props
❞
TypeScript
是什么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
包管理器来安装。
$ npm install -g typescript
$ tsc -v
Version 4.9.x // TS最新版本
$ tsc helloworld.ts
helloworld.ts => helloworld.js
在上图中包含 3 个 ts ⽂件:a.ts
、b.ts
和 c.ts
。这些⽂件将被 TypeScript
编译器,根据配置的编译选项编译成 3 个 js ⽂件,即 a.js
、b.js
和 c.js
。对于⼤多数使⽤ TypeScript
开发的 Web 项⽬,我们还会对编译⽣成的 js ⽂件进⾏「打包处理」,然后进⾏部署。
TypeScript 主要有 3 大特点:
TypeScript
可以编译出纯净、 简洁的 JavaScript
代码,并且可以运行在任何浏览器上、Node.js
环境中和任何支持 ECMAScript 3
(或更高版本)的JavaScript 引擎中。JavaScript
开发者在开发 JavaScript
应用程序时使用高效的开发工具和常用操作比如静态检查和代码重构。TypeScript
提供最新的和不断发展的 JavaScript
特性,包括那些来自 2015 年的 ECMAScript 和未来的提案中的特性,比如异步功能和 Decorators,以帮助建立健壮的组件。泛型Generics 是TS
中的一个重要部分,这篇文章就来简单介绍一下其概念并在React
中的应用。
❝泛型指的是「类型参数化」:即将原来某种具体的类型进⾏参数化 ❞
软件⼯程中,我们不仅要创建⼀致的、定义良好的 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
看到 <T>
语法,就「像传递参数⼀样」,上面代码传递了我们想要⽤于特定函数调⽤的类型。
参考上⾯的图⽚,当我们调⽤ identity<Number>(1)
, Number
类型就像参数 1 ⼀样,它将「在出现 T
的任何位置填充该类型」。图中 <T>
内部的 T
被称为「类型变量」,它是我们希望传递给 identity
函数的「类型占位符」,同时它被分配给 value
参数⽤来代替它的类型:此时 T 充当的是类型,⽽不是特定的 Number 类型。
其中 T
代表 Type
,在定义泛型时通常⽤作第⼀个类型变量名称。但实际上 T
可以⽤任何有效名称代替。除了 T
之外,以下是常⻅泛型变量代表的意思:
也可以引⼊希望定义的「任何数量的类型变量」。⽐如我们引⼊⼀个新的类型变量 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
接⼝的任何类型。
在前面的例子中,我们只举例了如何用泛型定义常规的函数语法,而不是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
的函数类型定义。
function useState<S>(
initialState: S | (() => S)
): [S, Dispatch<SetStateAction<S>>];
我们抽丝剥茧的来分析一下这个类型的定义。
useState
)它接受一个叫做S
的泛型变量initialState
(初始状态)S
(传入泛型)的变量,也可以是一个返回类型为S
的函数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
,并且不返回任何东西的函数。
现在我们已经理解了泛型的概念,我们可以看看如何在React代码中应用它。
❝
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
相关的分析处理。
假设,你正在为一个表单构建一个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
可以是一个数字或一个字符串,而不是其他?
「分享是一种态度」。
参考资料: