TS4技术笔记
一、TS4类型系统之基础使用
1.章节介绍
小伙伴大家好,本章将学习TS4类型系统之基础使用 - TS4带来的编码优势。
本章学习目标
本章将全面了解什么是TypeScript,给我们带来了什么好的编程体验,并对TypeScript中基础类型进行学习和掌握。对于后续的TS高级学习,以及与Vue、React结合开发都打下一个良好的基础!
课程安排
- 02-为什么使用TS和TS运行环境搭建
- 03-类型声明空间与变量声明空间
- 04-类型注解与类型推断
- 05-类型分类与联合类型与交叉类型
- 06-never类型与any类型与unknown类型
- 07-类型断言与非空断言
- 08-数组类型与元组类型
- 09-对象类型与索引签名
- 10-函数类型与void类型
- 11-函数重载与可调用注解
- 12-枚举类型与const枚举
2.为什么使用TS和TS运行环境搭建
TypeScript是微软开发的一个开源的编程语言,通过在JavaScript的基础上添加静态类型定义构建而成。TypeScript通过TypeScript编译器或Babel转译为JavaScript代码,可运行在任何浏览器,任何操作系统。
TS文件需要编译成JS文件
首先我们的浏览器是不认识TS文件的,所以需要把TS编译成JS文件才可以,TS官网提供了一种方式,就是去全局安装typescript这个模块,命令如下:
npm install -g typescript
安装好后,就可以使用 tsc xxx.ts命名把TS文件自动转化成对应的JS文件了,在同级目录下就会生成一个xxx.js的文件,这个文件就是编译后的文件,浏览器是可以认识的。
在TS编译JS文件的时候
第一,在tsc命令进行转换操作的时候,是不能实时进行转换的,那么可以通过添加一个**-w的参数来完成实时转换**的操作
tsc xxx.ts -w
第二,在编译后,我们会发现TS文件中定义的变量会产生错误的波浪线,这主要是因为TS默认是全局环境下的,所以跟其他文件中的同名变量冲突了,那么该如何解决呢?可以把全局环境改成局部的环境,只需要把TS文件变成一个模块化的文件,那么变量就只在模块内起作用,这样就不会产生冲突,从而引发错误的波浪线。
let foo = 123;
export {};
第三,如果不想采用默认的编译方式,那么可以通过修改配置文件来改变一些默认的行为。配置文件需要叫做,tsconfig.json。
可以通过tsc --init命令自动化的创建tsconfig.json这个文件,这个文件中的配置选项非常的多,我们会在后面小节中给大家进行详解的讲解,这里先修改其中的一两个配置,来感受一下配置文件的一个作用。
{
“compilerOptions”: {
"outDir": "./dist", //编译后文件输出的位置
"module": "ES6", //转换后模块的风格
"target": "ES5" //转换成JS不同语法版本
},
"include": ["xxx.ts"] //只对那些文件进行编译
}
通过命令行输入tsc命令就可以自动去调用tsconfig.json这个文件了,非常的方便。
为什么要使用TS去写程序
TS编写代码的好处诸多,下面我们列出几点:
- 友好的IDE提示
- 强制类型,防止报错
- 语言编写更加严谨
- 快速查找到拼写错误
- JS的超集,扩展新功能
下面通过几个小例子来感受一下TS比JS要优秀的点,
let a = 123;
a.map(()=>{}) // error
这段代码在TS中是会报错的,因为a是一个数字,不会有map方法,而JS是不会给我们进行提示的。
let a = 1;
if(a === 1 && a === 2) { // error
}
这段代码在TS中是会报错的,因为从逻辑上来看这段代码并没有意义,因为a同一时间不可能既是1又是2。
let a = {
username: 'xiaoming',
age: 20
}
这段代码TS提示会非常的好,不会夹杂一些其他的提示信息,而且一旦单词写错了,TS会有非常好的提示效果。
3.类型声明空间与变量声明空间
变量声明空间
在JS中我们只有变量声明空间,比如let a = 123。但是在TS中我们不仅存在变量声明空间,而且还存在类型声明空间。下面就让我们一起去探索一下类型声明空间的特点吧。
类型声明空间
可以通过type关键字来定义类型声明空间,type在TS中叫做类型别名。为了能够更好的区分两个空间,所以人为规定类型空间定义的名字首字母要大写,例如:type A = number。
这里要注意,不要把两个空间进行互相赋值,会产生错误的,如下:
let a = 'hello';
type B = a; // error
type A = string;
let a = A; // error
但是在TS中,有的语法它既是变量声明空间也是类型声明空间,比如:类语法。
class Foo {} // 类在TS中既是变量声明空间,也是类型声明空间
let a = Foo; // success
type A = Foo; // success
那么两个空间之间的关系有是什么呢?下一小节来进行学习。
4.类型注解与类型推断
类型注解
通过把变量空间与类型空间结合在一起的操作,就叫做类型注解,具体语法是通过冒号连接在一起的。
let a: string = 'hello'
//也可以通过类型别名进行连接
type A = string
let a: A = 'hello'
这样定义的a变量就具备了字符串类型,那么我们尝试给a修改成其他类型就会报错,现在就强制a的类型为字符串了,这也是TS强制类型的特点。
那么我们不去进行类型注解,是不是就不会强制类型了呢?其实也会强制类型的,因为TS可以进行自动类型推断的。
类型推断
TS中会对不进行类型注解的变量,进行自动类型推断的,如下:
// let a: string -> 类型推断
let a = 'hello'
a = 123; // error
所以在TS中不管是类型注解还是自动类型推断,对于类型都不能轻易的做修改,这样可以保证变量的类型稳定,从而减少BUG的产生。
5.类型分类与联合类型与交叉类型
类型分类
在TS中会把类型大致划分为三部分:
- 基本类型:string number boolean null undefined symbol bigint
- 对象类型:[] {} function(){}
- TS新增类型:any never void unknown enum
对于基本类型,我们就稍微演示一下,毕竟都是JS已有的内容:
let a: string = 'hello'
let b: bigint = 1n;
对象类型也稍微演示一下,更具体的对象类型会在后面的小节中进行讲解:
let c: object = [];
c = function(){}; // success
联合类型
在TS中如何能够让一个变量支持多种不同类型呢?那么就可以采用联合类型来实现。
let a: string|number|boolean = 'hello';
a = 123;
在类型中除了有或的操作外,就一定有与的操作,那么在TS中实现类型与的行为叫做,交叉类型。
交叉类型
类型之间进行与的操作,不过一般在对象中进行使用,基本类型很少采用交叉类型。
type A = {
username: string
}
type B = {
age: number
}
let a: A&B = { username: 'xiaoming', age: 20 }
这里a变量,必须具备A、B两个共同指定的类型才可以。
6.never类型与any类型与unknown类型
在本小节中将学习一些TS中提供的新的类型,比如:never类型,any类型,unknown类型。
never类型
never类型表示永不存在的值的类型,当一个值不存在的时候就会被自动类型推断成never类型。
// let a: never -> 不能将类型“number”分配给类型“never”
let a: number & string = 123
在这段代码中a变量要求类型既是数字又是字符串,而值是一个123无法满足类型的需求,所以a会被自动推断成never类型。所以never类型并不常用,只是在出现问题的时候会被自动转成never。
有时候也可以利用never类型的特点,实现一些小技巧应用,例如可以实现判断参数是否都已被使用,代码如下:
function foo(n: 1 | 2 | 3) {
switch (n) {
case 1:
break
case 2:
break
case 3:
break
default:
let m: never = n; // 检测n是否可以走到这里,看所有值是否全部被使用到
break
}
}
any类型和unknown类型
any类型表示任意类型,而unknown类型表示为未知类型,是any类型对应的安全类型。
既然any表示任意类型,那么定义的变量可以随意修改其类型,这样带来的问题就是TS不再进行类型强制,整个使用方式根JS没有任何区别。
let a: any = 'hello';
a = 123;
a = true;
a.map(()=>{}) // success
所以说any类型是TS中的后门,不到万不得已的时候尽量要少用,如果真的有这种需求的话,可以采用any对应的安全类型unknown来进行定义。
let a: unknown = 'hello';
a = 123;
// any不进行检测了,unknown使用的时候,TS默认会进行检测
a.map(()=>{}) // error
unknown类型让程序使用的时候更加严谨,我们必须主动告诉TS,这里是一个什么类型,防止我们产生误操作。那么怎样让unknown类型不产生错误呢?就需要配合类型断言去使用,下一个小节我们一起来学习吧。
7.类型断言与非空断言
类型断言
类型断言主要用于当 TypeScript 推断出来类型并不满足你的需求,你需要手动指定一个类型。在上一个小节中使用unknown类型的时候,需要手动指定当前是一个什么类型,来满足TS类型的需求检测,那么就可以采用类型断言来实现。
let a: unknown = 'hello';
a = 123;
(a as []).map(()=>{}) // success
这里就不会再报错了,当然类型断言只是告诉TS不要管我们了,这个只是在编译前的处理,就是欺骗TS。而在编译后,a确实不是一个数组,所以运行到浏览器后就会报错,那么我们在使用断言的时候还是要格外的小心,不要滥用断言操作。
非空断言
在类型断言中,也有一些特殊情况,如下:
let b: string|undefined = undefined;
b.length // error
因为b可能是字符串也可能是undefined,所以b.length的时候就会报错,这样我们可以采用非空断言来告诉TS,这个b肯定不是undefined,所以b只能是字符串,那么b.length就不会报错了。
let b: string|undefined = undefined;
b!.length // success
总结:类型断言是一种欺骗TS的手段,在编译阶段解决类型问题的,但是最终运行的结果需要开发人员自己负责,所以使用类型断言要严谨,否则最终还会产生报错。
8.数组类型与元组类型
在前面小节中介绍过对象类型,但是对象类型的范围太广了,本小节将具体讲解对象类型中的数组类型及元组类型。
数组类型
定义数组类型通常用两种写法:
- 类型[]
- Array<类型>
先看第一种方式,如下:
let arr1: (number|string)[] = [1, 2, 3, 'hello'];
再看第二种方式,如下:
let arr2: Array<number|string> = [1, 2, 3, 'hello'];
第二种写法是一种泛型的写法,这个会在后面章节中进行详解的。
这里数组子项可以是数字也可以是字符串。对于数组子项的顺序以及数组子项的个数并没有限制。那么如何能够对数组的个数以及顺序都做限定类型呢?就可以采用元组类型。
元组类型
元组类型实际上就是数组类型的一种特殊形式,代码如下:
let arr3: [number, string] = [1, 'hello'];
这里会限定数组的每一项的值的类型和值的个数,对于多添加一些子项都是会报错的,属于比较严格的数组形式。
9.对象类型与索引签名
对象类型
在TS中可以直接对对象字面量进行类型限定,可以精确到具体的字段都具备哪些类型,如下:
type A = {
username: string
age: number
}
let a: A = {
username: 'xiaoming',
age: 20
}
对于对象类型来说,多出来的字段还是缺少的字段都会产生错误,如下:
type A = {
username: string
age: number
}
let a: A = { // error
username: 'xiaoming'
}
那么可以给age添加一个可选标识符?来表示age为可选项,写与不写都是可以的。
type A = {
username: string
//age是可选项
age?: number
}
let a: A = { // success
username: 'xiaoming'
}
索引签名
那么对于多出来的字段,可以通过索引签名方式来解决,如下:
type A = {
username: string
//索引签名
[index: string]: any
}
let a: A = {
username: 'xiaoming'
gender: 'male',
job: 'it'
}
索引签名中的属性也可以指定number类型,不过往往只有数组中会采用这种数字类型的索引签名方式,如下:
type A = {
[index: number]: any
}
let a: A = [1, 2, 3, true, 'hello'];
对象类型如果想定义初始值为空值的话,可以采用类型断言来改造,如下:
type Obj = {username: string}
let obj = {} as Obj; // success
最后来看一下对象和数组组合在一起定义的类型,如下:
let json: {username: string, age: number}[] = [];
10.函数类型与void类型
函数类型
在TS中对于函数的类型使用方式有很多种限定,先来看一下函数的参数:
function foo(n: number, m?: string): number{
return 123;
}
foo(123, 'hello');
foo(123); // success
TS中要求,实参的个数跟形参的个数必须相同,所以可以添加可选链?来实现参数可选操作。
函数还可以通过在函数中定义返回值的类型:number,可以参考上面代码。
下面看一下函数表达式的写法,及类型注解的方式。
let foo: (n: number, m: string) => number = function(n, m){
return 123;
}
如果在前面进行了类型注解,那么就不用在函数体内进行类型的添加了。
void类型
表示函数没有任何返回值的时候得到的类型。
let foo = function(){ // void
}
当函数没有return的时候返回void类型,当return undefined的时候也可以返回void类型。那么函数定义void类型跟undefined类型也是有区别的,因为undefined 类型是不能不写return的。
let foo = function(): undefined{ // undefined 不能不写return的
} // error
11.函数重载与可调用注解
函数重载
函数重载是指函数约束传入不同的参数,返回不同类型的数据,而且可以清晰的知道传入不同的参数得到不同的结果。
假如我们实现这样一个函数,如下:
function foo(n1: number, n2?: number, n3?: number, n4?: number){
}
foo(1);
foo(1, 2);
foo(1, 2, 3);
foo(1, 2, 3, 4);
这样传递几个参数都是可以的,那么我们能不能限制传递几个参数呢?比如只允许传递一个参数,两个参数,四个参数,不允许传递三个参数,这时就可以通过函数重载还实现了。
function foo(n1: number): any
function foo(n1: number, n2: number): any
function foo(n1: number, n2: number, n3: number, n4: number): any
function foo(n1: number, n2?: number, n3?: number, n4?: number){
}
foo(1);
foo(1, 2);
foo(1, 2, 3); // error
foo(1, 2, 3, 4);
下面再来实现一个需求,要求参数的类型必须保持一致,如下:
function foo(n: number, m: number): any
function foo(n: string, m: string): any
function foo(n: number|string, m: number|string){
}
foo(1, 2);
foo('a', 'b');
foo(3, 'c'); // error
可调用注解
可调用注解提供多种调用签名,用以特殊的函数重载。首先可调用注解跟普通函数的类型注解可以起到同样的作用。
type A = () => void;
type A = { // 可调用注解,可以针对函数重载进行类型注解的
(): void
}
let a: A = () => {};
那么可调用注解比普通类型注解优势在哪里呢?就是可以对函数重载做类型注解,代码如下:
type A = {
(n: number, m: number): any
(n: string, m: string): any
}
function foo(n: number, m: number): any
function foo(n: string, m: string): any
function foo(n: number|string, m: number|string){
}
12.枚举类型与const枚举
枚举类型
枚举是组织收集有关联集合的一种方式,使代码更加易于阅读。其实简单来说枚举就是定义一组常量。
enum Roles {
SUPER_ADMIN,
ADMIN = 3,
USER
}
console.log( Roles.SUPER_ADMIN ); // 0
console.log( Roles.ADMIN ); // 3
console.log( Roles.USER ); // 4
枚举默认不给值的情况下,就是一个从0开始的数字,是可以自动进行累加的,当然也可以自己指定数值,后面的数值也是可以累加的。
枚举也支持反向枚举操作,通过数值来找到对应的key属性,这样操作起来会非常的灵活。
enum Roles {
SUPER_ADMIN,
ADMIN = 3,
USER
}
console.log( Roles[0] ); // SUPER_ADMIN
console.log( Roles[3] ); // ADMIN
console.log( Roles[4] ); // USER
枚举给我们的编程带来的好处就是更容易阅读代码,举例如下:
if(role === Roles.SUPER_ADMIN){ // 更容易阅读
}
下面来看一下,如果定义成字符串的话,需要注意一些什么?
enum Roles {
SUPER_ADMIN = 'super_admin',
ADMIN = 'admin',
USER = 'user'
}
字符串形式是没有默认值的,而且不能做反向映射的。
const枚举
在枚举的前面可以添加一个const关键字。
const enum Roles {
SUPER_ADMIN = 'super_admin',
ADMIN = 'admin',
USER = 'user'
}
那么没有const关键字和有const关键字的区别是什么呢?主要区别在于编译的最终结果,const方式最终编译出来的就是一个普通字符串,并不会产生一个对象,更有助于性能的体现。
章节总结
小伙伴大家好,本章学习了TS4类型系统之基础使用 - TS4带来的编码优势。
总结内容
- 了解了什么是TypeScript,在编程中的优势
- 如何对TS文件进行编译,以及tsconfig.json的使用
- 全面掌握TS中的各种概念,如:类型注解、类型断言等
- 全面掌握TS中常见类型,如:数组、对象、函数等
- 全面掌握TS中的新类型,如:枚举、元组、any、never等
二、TS4类型系统之进阶使用
1.章节介绍
小伙伴大家好,本章将学习TS4类型系统之进阶使用 - 带您探索类型的世界。
本章学习目标
本章将深入了解TypeScript语言中的类型系统,通过简单的示例来了解抽象的概念。通过掌握本章的内容,将轻松驾驭TS中的类型操作!
课程安排
- 02-详解接口与类型别名之间区别
- 03-字面量类型和keyof关键字
- 04-类型保护与自定义类型保护
- 05-定义泛型和泛型常见操作
- 06-类型兼容性详解
- 07-映射类型与内置工具类型
- 08-条件类型和infer关键字
- 09-类中如何使用类型
2.详解接口与类型别名之间区别
接口
接口是一系列抽象方法的声明,是一些方法特征的集合。简单来说,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。
接口跟类型别名类似都是用来定义类型注解的,接口是用interface关键字来实现的,如下:
interface A {
username: string;
age: number;
}
let a: A = {
username: 'xiaoming',
age: 20
}
因为接口跟类型别名功能类似,所以接口也具备像索引签名,可调用注解等功能。
interface A {
[index: number]: number;
}
let a: A = [1, 2, 3];
interface A {
(): void;
}
let a: A = () => {}
接口与别名的区别
那么接口跟类型别名除了很多功能相似外,他们之间也是具备某些区别的。
- 对象类型
- 接口合并
- 接口继承
- 映射类型
第一个区别**,类型别名可以操作任意类型,而接口只能操作对象类型**。
第二个区别,接口可以进行合并操作。
interface A {
username: string;
}
interface A {
age: number;
}
let a: A = {
username: 'xiaoming',
age: 20
}
第三个区别,接口具备继承能力。
interface A {
username: string
}
interface B extends A {
age: number
}
let b: B = {
username: 'xiaoming',
age: 20
}
B这个接口继承了A接口,所以B类型就有了username这个属性。在指定类型的时候,b变量要求同时具备A类型和B类型。
第四个区别,接口不具备定义成接口的映射类型,而别名是可以做成映射类型的,关于映射类型的用法后面小节中会详细的进行讲解,这里先看一下效果。
type A = { // success
[P in 'username'|'age']: string;
}
interface A { // error
[P in 'username'|'age']: string;
}
3.字面量类型和keyof关键字
字面量类型
在TS中可以把字面量作为具体的类型来使用,当使用字面量作为具体类型时, 该类型的取值就必须是该字面量的值。
type A = 1;
let a: A = 1;
这里的A对应一个1这样的值,所以A类型就是字面量类型,那么a变量就只能选择1作为可选的值,除了1作为值以外,那么其他值都不能赋值给a变量。
那么字面量类型到底有什么作用呢?实际上字面量类型可以把类型进行缩小,只在指定的范围内生效,这样可以保证值不易写错。
type A = 'linear'|'swing';
let a: A = 'ease' // error
比如a变量,只有两个选择,要么是linear要么是swing,不能是其他的第三个值作为选项存在。
keyof关键字
在一个定义好的接口中,想把接口中的每一个属性提取出来,形成一个联合的字面量类型,那么就可以利用keyof关键字来实现。
interface A {
username: string;
age: number;
}
//keyof A -> 'username'|'age'
let a: keyof A = 'username';
如果我们利用typeof语法去引用一个变量,可以得到这个变量所对应的类型,如下:
let a = 'hello';
type A = typeof a; // string
那么利用这样一个特性,可以通过一个对象得到对应的字面量类型,把typeof和keyof两个关键字结合使用。
let obj: {
username: 'xiaoming',
age: 20
}
let a: keyof typeof obj = 'username'
4.类型保护与自定义类型保护
类型保护
类型保护允许你使用更小范围下的对象类型。这样可以缩小类型的范围保证类型的正确性,防止TS报错。这段代码在没有类型保护的情况下就会报错,如下:
function foo(n: string|number){
n.length // error
}
因为n有可能是number,所以TS会进行错误提示,可以利用类型断言来解决,但是这种方式只是欺骗TS,如果在运行阶段还是可能报错的,所以并不是最好的方式。利用类型保护可以更好的解决这个问题。
类型保护的方式有很多种,主要是四种方式:
- typeof关键字
- instanceof关键字
- in关键字
- 字面量类型
typeof关键字实现类型保护:
function foo(n: string|number){
if(typeof n === 'string'){
n.length // success
}
}
instanceof关键字实现类型保护,主要是针对类进行保护的:
class Foo {
username = 'xiaoming'
}
class Bar {
age = 20
}
function baz(n: Foo|Bar){
if( n instanceof Foo ){
n.username
}
}
in关键字实现类型保护,主要是针对对象的属性保护的:
function foo(n: { username: string } | { age: number }){
if( 'username' in n ){
n.username
}
}
字面量类型保护,如下:
function foo(n: 'username'|123){
if( n === 'username' ){
n.length
}
}
自定义类型保护
除了以上四种方式可以做类型保护外,如果我们想自己去实现类型保护可行吗?答案是可以的,只需要利用is关键字即可, is为类型谓词,它可以做到类型保护。
function isString(n: any): n is string{
return typeof n === 'string';
}
function foo(n: string|number){
if( isString(n) ){
n.length
}
}
5.定义泛型和泛型常见操作
定义泛型
泛型是指在定义函数、接口或者类时,未指定其参数类型,只有在运行时传入才能确定。泛型简单来说就是对类型进行传参处理。
type A<T = string> = T //泛型默认值
let a: A = 'hello'
let b: A<number> = 123
let c: A<boolean> = true
这里可以看到通过<T>来定义泛型,还可以给泛型添加默认值<T=string>,这样当我们不传递类型的时候,就会已string作为默认的类型进行使用。
泛型还可以传递多个,实现多泛型的写法。
type A<T, U> = T|U; //多泛型
在前面我们学习数组的时候,讲过数组有两种定义方式,除了基本定义外,还有一种泛型的写法,如下:
let arr: Array<number> = [1, 2, 3];
//自定义MyArray实现
type MyArray<T> = T[];
let arr2: MyArray<number> = [1, 2, 3];
泛型在函数中的使用:
function foo<T>(n: T){
}
foo<string>('hello');
foo(123); // 泛型会自动类型推断
泛型跟接口结合的用法:
interface A<T> {
(n?: T): void
default?: T
}
let foo: A<string> = (n) => {}
let foo2: A<number> = (n) => {}
foo('hello')
foo.default = 'hi'
foo2(123)
foo2.default = 123
泛型与类结合的用法:
class Foo<T> {
username!: T;
}
let f = new Foo<string>();
f.username = 'hello';
class Foo<T> {
username!: T
}
class Baz extends Foo<string> {}
let f = new Baz()
f.username = 'hello'
有时候也会对泛型进行约束,可以指定哪些类型才能进行传递:
type A = {
length: number
}
function foo<T extends A>(n: T) {}
foo(123) // error
foo('hello')
通过extends关键字可以完成泛型约束处理。
6.类型兼容性详解
类型兼容性
类型兼容性用于确定一个类型是否能赋值给其他类型。如果是相同的类型是可以进行赋值的,如果是不同的类型就不能进行赋值操作。
let a: number = 123;
let b: number = 456;
b = a; // success
let a: number = 123;
let b: string = 'hello';
b = a; // error
当有类型包含的情况下,又是如何处理的呢?
let a: number = 123;
let b: string | number = 'hello';
//b = a; // success
a = b; // error
变量a是可以赋值给变量b的,但是变量b是不能赋值给变量a的,因为b的类型包含a的类型,所以a赋值给b是可以的。
在对象类型中也是一样的处理方式,代码如下:
let a: {username: string} = { username: 'xiaoming' };
let b: {username: string; age: number} = { username: 'xiaoming', age: 20 };
a = b; // success
b = a; // error
b的类型满足a的类型,所以b是可以赋值给a的,但是a的类型不能满足b的类型,所以a不能赋值给b。所以看下面的例子就明白为什么这样操作是可以的。
function foo(n: { username: string }) {}
foo({ username: 'xiaoming' }) // success
foo({ username: 'xiaoming', age: 20 }) // error
let a = { username: 'xiaoming', age: 20 }
foo(a) // success
这里把值存成一个变量a,再去进行传参就是利用了类型兼容性做到的。
7.映射类型与内置工具类型
映射类型
可以将已知类型的每个属性都变为可选的或者只读的。简单来说就是可以从一种类型映射出另一种类型。这里我们先要明确一点,映射类型只能用类型别名去实现,不能使用接口的方式来实现。
先看一下在TS中是如何定义一个映射类型的。
type A = {
username: string
age: number
}
type B<T> = {
[P in keyof T]: T[P]
}
type C = B<A>
这段代码中类型C与类型A是完全一样的,其中in关键字就类似于一个for in循环,可以处理A类型中的所有属性记做p,然后就可以得到对应的类型T[p]。
那么我们就可以通过添加一些其他语法来实现不同的类型出来,例如让每一个属性都是只读的,可以给每一项前面添加readonly关键字。
type B<T> = {
readonly [P in keyof T]: T[P]
}
内置工具类型
每次我们去实现这种映射类型的功能是非常麻烦的,所以TS中给我们提供了很多常见的映射类型,这些内置的映射类型被叫做,内置工具类型。
Readonly就是跟我们上边实现的映射类型是一样的功能,给每一个属性做成只读的。
type A = {
username: string
age: number
}
/* type B = {
readonly username: string;
readonly age: number;
} */
type B = Readonly<A>
Partial可以把每一个属性变成可选的。
type A = {
username: string
age: number
}
/* type B = {
username?: string|undefined;
age?: number|undefined;
} */
type B = Partial<A>
Pick可以把某些指定的属性给筛选出来。
type A = {
username: string
age: number
gender: string
}
/* type D = {
username: string;
age: number;
} */
type D = Pick<A, 'username'|'age'>
Record可以把字面量类型指定为统一的类型。
/* type E = {
username: string;
age: string;
} */
type E = Record<'username'|'age', string>
Required可以把对象的每一个属性变成必选项。
type A = {
username?: string
age?: number
}
/* type B = {
username: string;
age: number;
} */
type B = Required<A>
Omit是跟Pick工具类相反的操作,把指定的属性进行排除。
type A = {
username: string
age: number
gender: string
}
/* type D = {
gender: string
} */
type D = Omit<A, 'username'|'age'>
Exclude可以排除某些类型,得到剩余的类型。
// type A = number
type A = Exclude<string | number | boolean, string | boolean>
我们的内置工具类型还有一些,如:Extract、NonNullable、Parameters、ReturnType等,下一个小节中将继续学习剩余的工具类型。
8.条件类型和infer关键字
在上一个小节中,学习了Exclude这个工具类型,那么它的底层实现原理是怎样的呢?
type Exclude<T, U> = T extends U ? never : T;
这里可以看到Exclude利用了 ? : 的写法来实现的,这种写法在TS类型中表示条件类型,让我们一起来了解下吧。
条件类型
条件类型就是在初始状态并不直接确定具体类型,而是通过一定的类型运算得到最终的变量类型。
type A = string
type B = number | string
type C = A extends B ? {} : []
条件类型需要使用extends关键字,如果A类型继承B类型,那么C类型得到问号后面的类型,如果A类型没有继承B类型,那么C类型得到冒号后面的类型,当无法确定A是否继承B的时候,则返回两个类型的联合类型。
那么大多数情况下,条件类型还是在内置工具类型中用的比较多,就像上面的Exclude方法,下面就让我们一起看一下其他内置工具类型该如何去用吧。
Extract跟Exclude正好相反,得到需要筛选的类型。
// type Extract<T, U> = T extends U ? T : never -> 实现原理
// type A = string
type A = Extract<string | number | boolean, string>
NonNullable用于排除null和undefined这些类型。
//type NonNullable<T> = T extends null | undefined ? never : T; -> 实现原理
//type A = string
type A = NonNullable<string|null|undefined>
Parameters可以把函数的参数转成对应的元组类型。
type Foo = (n: number, m: string) => string
//type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never; -> 实现原理
// type A = [n: number, m: string]
type A = Parameters<Foo>
在Parameters方法的实现原理中,出现了一个infer关键字,它主要是用于在程序中对类型进行定义,通过得到定义的p类型来决定最终要的结果。
ReturnType可以把函数的返回值提取出类型。
type Foo = (n: number, m: string) => string
//type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any; -> 实现原理
//type A = string
type A = ReturnType<Foo>
这里也通过infer关键字定义了一个R类型,对应的就是函数返回值的类型。通过infer关键字可以在泛型之外也可以定义类型出来。
下面再利用infer来实现一个小功能,定义一个类型方法,当传递一个数组的时候返回子项的类型,当传递一个基本类型的时候就返回这个基本类型。
type A<T> = T extends Array<infer U> ? U : T
// type B = number
type B = A<Array<number>>
// type C = string
type C = A<string>
这里的U就是自动推断出的数组里的子元素类型,那么就可以完成我们的需求。
9.类中如何使用类型
本小节主要讲解在类中如何使用TS的类型,对于类的一些功能使用方式,例如:类的修饰符、混入、装饰器、抽象类等等并不做过多的介绍。
类中定义类型
属性必须给初始值,如果不给初始值可通过非空断言来解决。
class Foo {
username!: string;
}
给初始值的写法如下:
class Foo {
//第一种写法
//username: string = 'xiaoming';
//第二种写法
// username: string;
// constructor(){
// this.username = 'xiaoming';
// }
//第三种写法
username: string;
constructor(username: string){
this.username = username;
}
}
类中定义方法及添加类型也是非常简单的。
class Foo {
...
showAge = (n: number): number => {
return n;
}
}
类使用接口
类中使用接口,是需要使用implements关键字。
interface A {
username: string
age: number
showName(n: string): string
}
class Foo implements A {
username: string = 'xiaoming'
age: number = 20
gender: string = 'male'
showName = (n: string): string => {
return n
}
}
在类中使用接口的时候,是一种类型兼容性的方式,对于少的字段是不行的,但是对于多出来的字段是没有问题的,比如说gender字段。
类使用泛型
class Foo<T> {
username: T;
constructor(username: T){
this.username = username;
}
}
new Foo<string>('xiaoming');
继承中用的也比较多。
class Foo<T> {
username: T;
constructor(username: T){
this.username = username;
}
}
class Bar extends Foo<string> {
}
最后来看一下,类中去结合接口与泛型的方式。
interface A<T> {
username: T
age: number
showName(n: T): T
}
class Foo implements A<string> {
username: string = 'xiaoming'
age: number = 20
gender: string = 'male'
showName = (n: string): string => {
return n
}
}
10.章节总结
小伙伴大家好,本章学习了TS4类型系统之进阶使用 - 带您探索类型的世界。
总结内容
- 了解类型中接口与类型别名之间的区别,及各种使用方式
- 掌握什么是泛型与泛型的应用场景,以及与其他语法的结合使用
- 了解了类型的一些高级用法,如:类型保护、类型兼容性
- 了解了类型的一些高级语法,如:映射类型、条件类型
- 全面学习TS中内置的工具类型,如: Partial、Readonly …等第
三、章TS4扩展内容与Vue3结合使用
1.章节介绍
小伙伴大家好,本章将学习TS4扩展内容和Vue3结合使用 - 融会贯通TS与Vue。
本章学习目标
本章将扩展了解TypeScript在工程项目中的模块使用方式及配置方式,重点还会讲解TypeScript与Vue3如何配合开发,通过本章学习将很好的掌握Vue3结合TS进行使用
课程安排
- 02-模块系统与命名空间
- 03-d.ts声明文件和declare关键字
- 04-@types和DefinitelyTyped仓库
- 05-lib.d.ts和global.d.ts
- 06-详解tsconfig.json配置文件
- 07-Vue选项式API中如何使用TS
- 08-Vue选项式API中组件通信使用TS
- 09-Vue组合式API中如何使用TS
- 10-Vue组合式API中组件通信使用TS
- 11-VueRouter路由如何使用TS
- 12-Vuex状态管理如何使用TS
- 13-Pinia状态管理如何使用TS
- 14-Element Plus中如何使用TS
2.模块系统与命名空间
概念
模块化开发是目前最流行的组织代码方式,可以有效的解决代码之间的冲突与代码之间的依赖关系,模块系统一般视为“外部模块”,而命名空间一般视为“内部模块”
模块系统
TS中的模块化开发跟ES6中的模块化开发并没有太多区别,像ES6中的导入、导出、别名等,TS都是支持的。TS跟ES6模块化的区别在于TS可以把类型进行模块化导入导出操作。
这里定义两个TS文件,分别为1_demo.ts和2_demo.ts。代码如下:
// 2_demo.ts
export type A = string
// 1_demo.ts
import type { A } from './2_demo'
关键字type可加可不加,一般导出类型的时候尽量加上,这样可以区分开到底是值还是类型。
TS除了支持ES6的模块化风格写法外,也支持require风格,但是使用的比较少,下面我们来了解一下。
// 2_demo.ts
type A = string
export = A
// 1_demo.ts
import A = require('./2_demo')
let a: A = 'hello'
下面来了解一下什么是模块化的动态引入,正常我们import导入方式是必须在顶部进行添加的,不能在其他语句中引入,这样就不能在后续的某个时机去导入,所以TS提供了动态引入模块的写法。
// 1_demo.ts
setTimeout(() => {
import('./2_demo').then(({ a }) => {
console.log(a)
})
}, 2000)
这种动态导入只支持值的导入,不支持类型的导入,这需要注意一下。
命名空间
模块化是外部组织代码的一种方式,而命名空间则是内部组织代码的一种方式。防止在一个文件中产生代码之间的冲突。
TS提供了namespace语法来实现命名空间,代码如下:
namespace Foo {
export let a = 123
}
namespace Bar {
export let a = 456
}
console.log(Foo.a)
console.log(Bar.a)
命名空间也是可以导出的,在另一个模块中可以导入进行使用,并且导出值和类型都是可以的。
// 2_demo.ts
export namespace Foo {
export let a = 123
export type A = string
export function foo() {}
export class B {}
}
// 1_demo.ts
import { Foo } from './2_demo'
console.log(Foo.a)
let a: Foo.A = 'hello world'
3.d.ts声明文件和declare关键字
d.ts声明文件
在 TypeScript 中以 .d.ts 为后缀的文件,我们称之为 TypeScript 声明文件。它的主要作用是描述 JavaScript 模块内所有导出接口的类型信息。
当我们开发了一个模块,我们需要让模块既可以适配JS项目,又可以适配TS项目,那么就可以利用.d.ts声明文件来实现,这样就可以让我们的JS模块在TS环境下进行使用了,而类型空间就交给声明文件来处理吧。
// 01_demo.js
function foo(n) {
console.log(n);
}
exports.foo = foo;
// 01_demo.d.ts
export declare function foo(n: number): void
这里可以看到declare这个关键词,就是在声明文件中进行类型定义的,这个只是用于定义类型,不会产生任何功能实现,具体的功能是由01_demo.js文件来实现的。
这样定义好了01_demo.js所配套的声明文件后,那么就可以把这个01_demo.js文件在TS文件中进行导入,并且正常的进行使用。
// 02_demo.ts
import { foo } from './01_demo'
foo(123) // ✔
foo('hello') // ✖
可以看到当往函数中传递不正确类型的时候,声明文件就会起作用,提示类型错误的信息。
声明文件总结来说,就是可以让我们的JS文件在TS中进行使用,从而适配JS和TS两个环境。
不过自己手写声明文件是比较麻烦的,所以当我们用TS去编写代码的时候,可以利用tsconfig.json文件自动创建转换后的声明文件。
// tsconfig.json
"declaration": true, // 打开注释后,自动生成.d.ts文件
这样当代码多了,我们也不用担心编写声明文件的问题了,让TS自动帮我们去生成就好了。
4.@types和DefinitelyTyped仓库
DefinitelyTyped仓库
DefinitelyTyped 是一个 高质量 的 TypeScript 类型定义的仓库。通过 @types方式来安装常见的第三方JavaScript库的声明适配模块。
仓库的在线地址为:https://github.com/borisyankov/DefinitelyTyped
那么这个仓库起到什么作用呢?在上一个小节中讲到,如果一个JS模块想要适配TS项目,那么需要有d.ts声明文件。那么如果这个JS模块没有提供声明文件的话,就可以通过DefinitelyTyped仓库下载第三方的声明文件来进行适配。
这个仓库会包含大部分常见JS库的声明文件,只需要下载就可以生效。下面我们举例,下载一个jquery库,并在TS项目引入jquery。
// 1_demo.ts
import $ from 'jquery // error,提示缺少声明文件
jquery库并没有默认提供d.ts声明文件,所以导入模块的时候肯定是要报错的。鼠标移入到错误上,提示的信息就有让我们去安装对应的第三方声明文件,即:npm i --save-dev @types/jquery
那么我们按照提示进行安装后,就会解决适配问题了,错误信息不再提示,并且jquery库的类型系统也会生效。
当然并不是所有的JS模块都需要下载第三方的@types,因为有些模块默认就会代码d.ts的声明文件,例如moment这个模块,安装好后,就会自带moment.d.ts文件。
5.lib.d.ts和global.d.ts
lib.d.ts
当你安装 TypeScript 时,会顺带安装一个 lib.d.ts 声明文件。这个文件包含 JavaScript 运行时以及 DOM 中存在各种常见的环境声明。
当我们使用一些原生JS操作的时候,也会拥有类型,代码如下:
let body: HTMLBodyElement | null = document.querySelector('body')
let date: Date = new Date()
这里的HTMLBodyElement和Date都是TypeScript下自带的一些内置类型,这些类型都存放在lib这个文件夹下。

global.d.ts
有时候我们也想扩展像lib.d.ts这样的声明类型,可以在全局下进行使用,所以TS给我们提供了global.d.ts文件使用方式,这个文件中定义的类型都是可以直接在全局下进行使用的,不需要模块导入。
// global.d.ts
type A = string
// 1_demo.ts
let a: A = 'hello' // ✔
let b: A = 123 // ✖
详解tsconfig.json配置文件
tsconfig.json
前面我们已经对tsconfig.json文件有了一些大致的了解,本小节我们来详解一下这个配置文件。
这个配置文件主要使用compilerOptions: {}进行TS的编译与转化。当然还有一些其他外层可配置的字段,如下:
{
"compilerOptions": {}, // 编译选项
"files": [], // 包含在程序中的文件的允许列表
"extends": "", // 继承的另一个配置文件
"include": [], // 指定的进行编译解析
"exclude": [], // 指定的不进行编译解析
"references": [] // 项目引用,提升性能
}
其中files和include都是指定哪些文件是可以进行编译的,只不过files指定的是比较少的文件,多文件的话可以用include来进行指定,当然如果要跳过哪些文件不进行编译,就可以利用exclude字段。
extends可以通过继承的方式去加载另一个配置文件,使用的情况并不是很多。references可以把编译分成一个一个独立的模块,这样是有助于性能的提升。这些选项都是顶层的,用的最多的还是compilerOptions字段。
compilerOptions
通过tsc --init会自动生成tsconfig.json文件,这个文件会默认带有6个选项配置,如下:
{
"compilerOptions": {
"target": "es2016", // 指定编译成的是哪个版本的js
"module": "commonjs", // 指定要使用的模块化的规范
"strict": true, // 所有严格检查的总开关
"esModuleInterop": true, // 兼容JS模块无default的导入
"skipLibCheck": true, // 跳过所有.d.ts文件的类型检查
"forceConsistentCasingInFileNames": true // 引入时强制区分大小写
}
}
除了初始的这些配置外,其他的配置都用注释给注释起来了,同时tsconfig.json把配置选项做了一些分类。
- Projects -> 项目
- Language and Environment -> 语言和环境
- Modules -> 模块
- JavaScript Support -> JS的支持
- Emit -> 发射
- Interop Constraints -> 操作约束
- Type Checking -> 类型检测
- Completeness -> 完整性
在Projects分类中,incremental表示增量配置,可以对编译进行缓存,下一次编译会在上一次编译的基础上完成,这样有助于性能;tsBuildInfoFile是增量编译的目录,生成一个缓存文件。
在Language and Environment分类中表示最终文件会编译成什么样子,target就是转化成JS的版本;jsx配置是可以指定tsx转换成jsx还是js。
在Modules 分类是用于控制模块的,module表示模块化转换后的风格,是ESM还是AMD还是CJS等;moduleResolution表示查找模块的方式,如果设置值为node表示查找模块的时候会找node_modules这个文件夹,如果选择其他的方式会导致查找模块的方式发生改变。
在JavaScript Support分类中主要是对JS进行一些配置的,allowJs表示是否允许对JS文件进行编译,默认是false,当开启为true的时候,可以把JS文件进行编译输出;checkJs表示可以对JS文件进行类型检测,如果类型发生改变就会有报错警告。
在Emit 分类中表示编译输出的情况,declaration表示是否生成d.ts文件;sourceMap表示是否生成.map文件。
在Interop Constraints分类中会对使用进行操作约束,esModuleInterop表示当模块不具备export default形式的时候也可以默认导入的方式来使用;forceConsistentCasingInFileNames表示模块引入的时候是否区分大小写。
在Type Checking分类表示对类型进行检测,strict表示是否开启严格模式,对类型检测会非常的严格,一般建议开启。在严格模式下限制是非常多的,例如:当一个变量是any类型的时候也要去指定一下类型;null不能成为其他类型的子类型,所以null不能随便赋值给其他类型等等。
在Completeness 分类表示是否具备完整性检测,skipLibCheck表示是否跳过对d.ts的类型检测,默认都是跳过的。
