最后
你要问前端开发难不难,我就得说计算机领域里常说的一句话,这句话就是『难的不会,会的不难』,对于不熟悉某领域技术的人来说,因为不了解所以产生神秘感,神秘感就会让人感觉很难,也就是『难的不会』;当学会这项技术之后,知道什么什么技术能做到什么做不到,只是做起来花多少时间的问题而已,没啥难的,所以就是『会的不难』。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
我特地针对初学者整理一套前端学习资料
文章目录
-
- 1、`TypeScript` 简介
-
- 1.1、什么是 `TypeScript`
-
1.2、为什么要使用 `TypeScript`
-
2、安装 `TypeScript`
-
- 2.1、使用 `tsc` 命令编译 `TypeScript` 代码
-
2.2、使用 `ts-node` 快速编译 `TypeScript`
-
2.3、使用工程化编译方案
-
3、`TypeScript` 基本数据类型
-
- 3.1、变量声明
-
3.2、`TypeScript` 基本类型
-
3.3、`BigInt` 类型
-
3.4、`Symbol` 类型
-
3.5、元组(`Tuple`)
-
3.6、枚举(`Enum`)
-
3.7、`Never` 类型
-
3.8、`unknown` 类型
-
4、接口(`Interface`)
-
- 4.1、接口的概念
-
4.2、应用场景
-
4.3、接口的好处
-
4.4、接口的属性
-
4.5、函数类型
-
4.6、可索引类型
-
4.7、类类型
-
4.8、继承接口
-
4.9、混合类型
-
5、类(Class)
-
- 5.1、类的概念
-
5.2、类的本质
-
5.3、类的继承
-
5.4、访问修饰符
-
5.5、静态方法
-
5.6、抽象类
-
5.7、把类当做接口使用
-
6、函数(Function)
-
- 6.1、函数的概念
-
6.2、函数类型
-
5.3、函数的参数
-
6.4、函数重载
-
6.5、使用函数时的注意事项
-
7、泛型(Generic)
-
- 7.1、泛型的概念
-
7.2、多个类型参数
-
7.3、泛型参数默认类型
-
7.4、泛型类型与泛型接口
-
7.5、泛类型
-
7.6、泛型约束
-
7.7、多重类型泛型约束
-
8、`TypeScript` 类型进阶
-
- 8.1、字面量类型
-
8.2、类型推断
-
8.3、类型断言
-
8.4、类型保护
-
8.5、类型兼容性
-
8.6、交叉类型
-
8.7、联合类型
-
8.8、类型别名
-
8.9、索引类型
-
8.10、映射类型
-
8.11、条件类型
-
9、迭代器(Iterator)
-
- 9.1、基本概念
-
9.2、模拟一个简单的迭代器
-
9.3、可迭代性
-
9.4、`for…of`
-
9.5、`for…of` 与 `for…in` 的区别
-
9.6、解构赋值与扩展运算符
-
10、生成器(Generator)
-
- 10.1、概念
-
10.2、生成器函数的特殊性
-
10.3、通过 `next()` 参数向生成器传值
-
11、装饰器(Decorator)
-
- 11.1、基本概念
-
11.2、装饰器的使用方法
-
11.3、类装饰器
-
11.4、作用于类属性的装饰器
-
11.5、方法参数装饰器
-
11.6、装饰器执行顺序
-
13、`TypeScript` 在 `React` 项目中的应用
-
- 13.1、创建支持 `TypeScript` 的 `React` 项目
-
13.2、`tsconfig.json` 配置文件详解
-
13.3、创建第一个 `Hello.tsx` 组件
-
13.4、自定义 `Button` 组件
1.1、什么是 TypeScript
TypeScript
不是一门全新的语言,TypeScript
是 JavaScript
的超集,它对 JavaScript
进行了一些规范和补充。所以,学习 TypeScript
需要有 JavaScript
的基础。
TypeScript
的特性:
-
TypeScript
是JavaScript
的超集,它可以编译成纯JavaScript
。 -
TypeScript
基于ECMAScript
标准进行拓展,支持ECMAScript
未来提案中的特性,如装饰器、异步功能等。 -
TypeScript
编译的JavaScript
可以在任何浏览器运行,TypeScript
编译工具可以运行在任何操作系统上。 -
TypeScript
起源于开发较大规模JavaScript
应用程序的需求。由微软在2012年发布了首个公开版本。
1.2、为什么要使用 TypeScript
静态类型
JavaScript
只会在运行时去做数据类型检查,而 TypeScript
作为静态类型语言,其数据类型是在编译期间确定的,编写代码的时候要明确变量的数据类型。使用 TypeScript
后,就不会出现因为数据类型错误导致的代码运行异常。
常见的 JavaScript
异常有:
Uncaught TypeError: Cannot read property
TypeError: ‘undefined’ is not an object
TypeError: null is not an object
TypeError: Object doesn’t support property
TypeError: ‘undefined’ is not a function
TypeError: Cannot read property ‘length’
三大框架支持
TypeScript
之所以能够流行起来并且保持长久的生命力,离不开前端三大框架的支持。
-
Angular
是TypeScript
最早的支持者,Angular
官方推荐使用TypeScript
来创建应用。 -
React
虽然经常与Flow
一起使用,但是对TypeScript
的支持也很友好。 -
Vue3.0
正式版即将发布,由TypeScript
编写。
从国内的氛围来看,由前端三大框架引领的 TypeScript
热潮已经涌来,很多招聘要求上也都有了 TypeScript
的身影。
兼容 JavaScript
TypeScript
虽然严谨,但没有丧失 JavaScript
的灵活性,TypeScript
非常包容:
-
TypeScript
通过tsconfig.json
来配置对类型的检查严格程度。 -
可以把
.js
文件直接重命名为.ts
。 -
可以通过将类型定义为
any
来实现兼容任意类型。 -
即使
TypeScript
编译错误,也可以生成JavaScript
文件。
这里先简单介绍下 any
类型,后续会详细讲解。比如一个 string
类型,在赋值过程中类型是不允许改变的:
let brand: string = ‘hello’
brand = 1 // Type ‘1’ is not assignable to type ‘string’.ts(2322)
代码块12
如果是 any
类型,则允许被赋值为任意类型,这样就跟我们平时写 JavaScript
一样了:
let brand: any = ‘hello’
brand = 1
代码块12
基于上面这些特点,一个熟悉 JavaScript
的工程师,在查阅一些 TypeScript
语法后,即可快速上手 TypeScript
。
2.1、使用 tsc
命令编译 TypeScript
代码
安装命令:
$ npm install -g typescript
或使用淘宝镜像
$ cnpm i -g typescript
查看当前 typescript
版本:
$ tsc -v
创建 index.ts
文件,示例代码如下:
const msg:string = ‘hello’
console.log(msg);
在当前文件所在目录运行命令:
$ tsc index.ts
会生成一个同名的 index.js
文件,然后在使用 node
命令运行 index.js
文件:
$ node index.js
运行成功即可以在控制台打印 hello
的内容。
2.2、使用 ts-node
快速编译 TypeScript
安装 ts-node
:
$ npm install -g ts-node
或
$ cnpm i -g ts-node
使用 ts-node
可以快速运行 .ts
文件,无需先转成 .js
文件后再运行,使用 ts-node
运行 index.ts
文件:
$ ts-node index.ts
如果运行 ts-node
命令报错,提示:Error: Cannot find module '@types/node/package.json'
,需要安装相关依赖:
$ npm install -g tslib @types/node
2.3、使用工程化编译方案
创建 ts-project
项目的目录,在当前项目根目录下启动命令行工具,初始化 package.json
文件:
$ npm init -y
在 package.json
中入口文件选项改为 src/index.ts
:
{
“main”: “src/index.ts”
}
然后使用 tsc
命令进行初始化:
$ tsc --init
在项目根目录下就会自动创建一个 tsconfig.json
文件,它指定了用来编译这个项目的根文件和编译选项。
对 tsconfig.json
配置文件做修改:
{
“compilerOptions”: {
“target”: “ESNext”, /* 支持 ES6 语法 */
“module”: “commonjs”,
“outDir”: “./lib”,
“rootDir”: “./src”,
“declaration”: true, /* 生成相应的.d.ts文件 */
“strict”: true,
“strictNullChecks”: false,
“noImplicitThis”: true
},
“exclude”: [“node_modules”, “lib”, “**/*.test.ts”],
“include”: [“src”]
}
在 package.json
文件中,加入 script
命令以及依赖关系:
{
“name”: “ts-project”,
“version”: “1.0.0”,
“description”: “”,
“main”: “src/index.ts”,
“scripts”: {
“tsc”: “tsc”
},
“author”: “”,
“license”: “ISC”,
“devDependencies”: {
“@types/node”: “^13.1.1”,
“typescript”: “^3.7.4”
}
}
下载相关依赖:
$ npm install
你会看到多了一个 node_modules
文件夹和一个 package-lock.json
文件,node_modules
文件夹是项目的所有依赖包,package-lock.json
文件将项目依赖包的版本锁定,避免依赖包大版本升级造成不兼容问题。
在 src/index.ts
文件中编写 TypeScript
代码:
export enum TokenType {
ACCESS = ‘accessToken’,
REFRESH = ‘refreshToken’
}
执行编译命令:
$ npm run tsc
这时候可以看到多了一个 lib
文件夹,里面的内容就是项目的编译结果了。
lib
文件夹中会生成两个文件:
-
index.d.ts
-
index.js
.d.ts
是声明文件,用于编写第三方类库,通过配置 tsconfig.json
文件中的 declaration
为 true
,在编译时可自行生成。
3.1、变量声明
TypeScript
是 JavaScript
的超集,同 JavaScript
一样,声明变量可以采用下面三个关键字:
-
var
-
let
-
const
详细解释可以查看 ES6
的相关教程。
在 TypeScript
中变量声明语法:冒号 :
前面是变量名称,后面是变量类型。例如:
let i:number = 1
3.2、TypeScript
基本类型
TypeScript
中的类型有:
-
原始类型
-
boolean
-
number
-
string
-
void
-
null
-
undefined
-
bigint
-
symbol
-
元组
tuple
-
枚举
enum
-
任意
any
-
unknown
-
never
-
数组
Array
-
对象
object
布尔类型
表示逻辑值:true
和 false
,声明一个布尔类型:
const boo: boolean = false
boo = 0 //报错
const done: boolean = Boolean(0)
数字类型
表示双精度 64 位浮点值,可以用来表示整数和分数。
let binaryLiteral: number = 0b1010; // 二进制
let octalLiteral: number = 0o744; // 八进制
let decLiteral: number = 6; // 十进制
let hexLiteral: number = 0xf00d; // 十六进制
字符串类型
一个字符串系列,使用单引号('
)或双引号("
)来表示字符串类型,反引号(`)来定义多行文本和内嵌表达式。
let name: string = “tom”;
let age: number = 5;
let words: string = 我叫 ${ name },今年 ${ age } 岁
;
任意类型
声明为 any
的变量可以赋值任意类型的值。
let str:any = ‘hello’
str = 12
str = true
str = [1,2,3,4]
任意值是 TypeScript
针对编程时类型不明确的变量使用的一种数据类型,常用于以下三种情况
1、变量的值会动态改变时,比如来自用于的输入,任意类型可以让这些变量跳过编译阶段的类型检查,示例代码如下:
let x: any = 1; // 数字类型
x = ‘I am who I am’; // 字符串类型
x = false; // 布尔类型
2、改写现有代码时,任意类型允许在编译时可选择地包含或移除类型检查,示例代码如下:
let x: any = 4;
x.ifItExists(); // 正确,ifItExists方法在运行时可能存在,但这里并不会检查
x.toFixed(); // 正确
3、定义存储各种类型数据的数组时,示例代码如下:
let arrayList: any[] = [1, false, ‘fine’];
arrayList[1] = 100;
联合类型
如果想要为一个变量赋值多种指定的类型,在声明变量时可以使用 |
为该变量指定多种联合类型。
let numOrStr:number | string = ‘hello’
numOrStr = 12
numOrStr = true // 报错
数组类型
声明变量为数组。
// 在元素类型后面加上[]
let arr: number[] = [1, 2];
arr.push(‘hello’) // 报错
// 或者使用数组泛型
let arr: Array = [1, 2];
arr = [1,2,‘hello’] // 报错
//混合元素类型
let list: any[] = [‘Sherlock’, 1887]
元组
元组类型用来表示已知元素数量和类型的数组,各元素的类型不必相同,对应位置的类型需要相同。
let x: [string, number];
x = [‘hello’, 1]; // 运行正常
x = [1, ‘hello’]; // 报错
x = [‘hello’, 1, true]; // 报错
console.log(x[0]); // 输出 hello
枚举
枚举类型用于定义数值集合。
数字枚举:
//数字枚举,枚举会从0开始,依次为元素赋值
enum Color{
Blue,
Red,
Green
}
console.log(Color.Blue); // 输出 0
//使用数字枚举
let a:Color = Color.Blue
console.log(a) // 输出 0
//反向映射
console.log(Color[0]); // 输出字符串类型的 Blue
可以为数字枚举手动赋值:
enum Color{
Blue = 10,
Red,
Green
}
console.log(Color.Blue); // 输出 10
console.log(Color.Red); // 输出 11
//反向映射
console.log(Color[0]); // 输出 undefined
字符串枚举:
enum Color{
Blue = ‘BLUE’,
Red = ‘RED’,
Green = ‘GREEN’
}
console.log(Color.Blue); // 输出字符串类型的 BLUE
console.log(Color.Red); // 输出字符串类型的 RED
let str = ‘RED’
console.log(str === Color.Red); // 输出 true
常量枚举
没有使用常量枚举之前编译的效果:
// index.ts
enum Color{
Blue = “BLUE”,
Red = ‘RED’,
Green = ‘GREEN’
}
console.log(Color.Blue);
编译后:
// index.js
var Color;
(function (Color) {
Color[“Blue”] = “BLUE”;
Color[“Red”] = “RED”;
Color[“Green”] = “GREEN”;
})(Color || (Color = {}));
console.log(Color.Blue);
使用常量枚举:
// index.ts
const enum Color{
Blue = “BLUE”,
Red = ‘RED’,
Green = ‘GREEN’
}
console.log(Color.Blue);
编译后:
// index.js
console.log(“BLUE” /* Blue */);
使用常量枚举可以提升编译的性能,不会将 ts
中的枚举编译成 JavaScrit
代码,而是直接使用常量值,这样就提高了性能。只有常量值才能使用常量枚举。
void
用于表示方法返回值的类型,表示该方法没有返回值,是一个返回值的占位符。
// 当方法没有返回值时,使用void作为占位符
function hello(): void {
alert(“Hello Runoob”);
}
声明一个 void
类型的变量没有什么用,只能为变量赋值 undefined
和 null
。
let nothing: void = undefined
null
表示对象值缺失。
在 JavaScript
中 null
表示 “什么都没有”。null
是一个只有一个值的特殊类型。表示一个空对象引用。
用 typeof
检测 null
返回是 object
。
undefined
用于初始化变量为一个未定义的值。
在 JavaScript
中, undefined
是一个没有设置值的变量。typeof
一个没有值的变量会返回 undefined
。
Null
和 Undefined
是其他任何类型(包括 void
)的子类型,可以赋值给其它类型,如数字类型,此时,赋值后的类型会变成 null
或 undefined
。而在 TypeScript
中启用严格的空校验( --strictNullChecks
)特性,就可以使得 null
和 undefined
只能被赋值给 void
或本身对应的类型,示例代码如下:
// 启用 --strictNullChecks
let x: number;
x = 1; // 运行正确
x = undefined; // 运行错误
x = null; // 运行错误
上面的例子中变量 x
只能是数字类型。如果一个类型可能出现 null
或 undefined
, 可以用 |
来支持多种类型,示例代码如下:
// 启用 --strictNullChecks
let x: number | null | undefined;
x = 1; // 运行正确
x = undefined; // 运行正确
x = null; // 运行正确
never
never
是其他类型(包括 null
和 undefined
)的子类型,代表从不会出现的值。这意味着声明为 never
类型的变量只能被 never
类型所赋值,在函数中它通常表现为抛出异常或无法执行到终止点(例如无限循环),示例代码如下:
let x: never;
let y: number;
// 运行错误,数字类型不能转为 never 类型
x = 123;
// 运行正确,never 类型可以赋值给 never类型
x = (()=>{ throw new Error(‘exception’)})();
// 运行正确,never 类型可以赋值给 数字类型
y = (()=>{ throw new Error(‘exception’)})();
// 返回值为 never 的函数可以是抛出异常的情况
function error(message: string): never {
throw new Error(message);
}
// 返回值为 never 的函数可以是无法被执行到的终止点的情况
function loop(): never {
while (true) {}
}
注意事项:
-
TypeScript
中描述类型要用小写
,比如boolean
、number
、string
等; -
大写开头的如
Boolean
、Number
、String
代表的是JavaScript
的构造函数;
//通过 new Number(‘10’) 得到的是一个构造函数,本质是一个对象
let a: Number = new Number(‘10’) // a === 10 为 false
// Number(‘10’) 与 10 都是声明一个数字 10 的方法,本质就是一个数字
let b: number = Number(‘10’) // b === 10 为 true
/*
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。a 是一个对象,它的 proto 属性指向该对象的构造函数的原型对象 Number,所以为 true。b 是一个数字,所以为 false。
*/
a instanceof Number // true
b instanceof Number // false
__proto__
是非标准属性,你也可以使用 Object.getPrototypeOf()
方法来访问一个对象的原型:
a.proto === Object.getPrototypeOf(a) // true
需要注意的是,TypeScript
一些基本类型,需要记住的是:TypeScript
中描述类型要用 小写。不要滥用 any
!
3.3、BigInt
类型
概念
bigint
是一种基本数据类型(primitive data type
)。
JavaScript
中可以用 Number
表示的最大整数为 2^53 - 1
,可以写为 Number.MAX_SAFE_INTEGER
。如果超过了这个界限,可以用 BigInt
来表示,它可以表示任意大的整数。
语法
在一个整数字面量后加 n
的方式定义一个 BigInt
,如:10n
或者调用函数 BigInt()
:
const theBiggestInt: bigint = 9007199254740991n
const alsoHuge: bigint = BigInt(9007199254740991)
const hugeString: bigint = BigInt(“9007199254740991”)
theBiggestInt === alsoHuge // true
theBiggestInt === hugeString // true
BigInt
与 Number
的不同点:
-
BigInt
不能用于Math
对象中的方法。 -
BigInt
不能和任何Number
实例混合运算,两者必须转换成同一种类型。 -
BigInt
变量在转换为Number
变量时可能会丢失精度。
使用 number
类型:
const biggest: number = Number.MAX_SAFE_INTEGER
//最大精度就是这个容器已经完全满了,无论往上加多少都会溢出,所以这两个值是相等的
const biggest1: number = biggest + 1
const biggest2: number = biggest + 2
biggest1 === biggest2 // true 超过精度
使用 BigInt
类型:
const biggest: bigint = BigInt(Number.MAX_SAFE_INTEGER)
//bigint 类型就是用来表示那些已经超出了 number 类型最大值的整数值,也就是这个容器还没满,在此基础上加上两个不同的值,其结果不相等
const biggest1: bigint = biggest + 1n
const biggest2: bigint = biggest + 2n
biggest1 === biggest2 // false
类型信息
使用 typeof
检测类型时,BigInt
对象返回 bigint
:
typeof 10n === ‘bigint’ // true
typeof BigInt(10) === ‘bigint’ // true
typeof 10 === ‘number’ // true
typeof Number(10) === ‘number’ // true
运算
BigInt
可以正常使用 +
、-
、*
、/
、**
、%
符号进行运算:
const previousMaxSafe: bigint = BigInt(Number.MAX_SAFE_INTEGER) // 9007199254740991n
const maxPlusOne: bigint = previousMaxSafe + 1n // 9007199254740992n
const multi: bigint = previousMaxSafe * 2n // 18014398509481982n
const subtr: bigint = multi – 10n // 18014398509481972n
const mod: bigint = multi % 10n // 2n
const bigN: bigint = 2n ** 54n // 18014398509481984n
当使用 /
操作符时,会向下取整,不会返回小数部分:
const divided: bigint = 5n / 2n // 2n, not 2.5n
比较 与 条件
Number
和 BigInt
可以进行比较:
0n === 0 // false
0n == 0 // true
1n < 2 // true
2n > 1 // true
2 > 2 // false
2n > 2 // false
2n >= 2 // true
条件判断:
if (0n) {
console.log(‘条件成立!’);
} else {
console.log(‘条件不成立!’); // 输出结果
}
0n || 10n // 10n
0n && 10n // 0n
Boolean(0n) // false
Boolean(10n) // true
!10n // false
!0n // true
3.4、Symbol
类型
概念
symbol
是一种基本数据类型(primitive data type
)。
Symbol()
函数会返回 symbol
类型的值。每个从 Symbol()
返回的 symbol
值都是唯一的。
语法
Symbol([description])
参数 description
:可选的,字符串类型。
使用 Symbol()
创建新的 symbol
类型:
const sym1: symbol = Symbol()
const sym2: symbol = Symbol(‘foo’)
const sym3: symbol = Symbol(‘foo’)
上面的代码创建了三个新的 symbol
类型,但要注意每个从 Symbol()
返回的值都是唯一的:
console.log(sym2 === sym3) // false
每个 Symbol()
方法返回的值都是唯一的,所以,sym2
和 sym3
不相等。
Symbol()
作为构造函数是不完整的:
const sym = new Symbol() // TypeError
这种语法会报错,是因为从 ECMAScript 6
开始围绕原始数据类型创建一个显式包装器对象已不再被支持,但因历史遗留原因, new Boolean()
、new String()
以及 new Number()
仍可被创建:
const symbol = new Symbol() // TypeError
const bigint = new BigInt() // TypeError
const number = new Number() // OK
const boolean = new Boolean() // OK
const string = new String() // OK
应用场景
1、当一个对象有较多属性时(往往分布在不同文件中由模块组合而成),很容易将某个属性名覆盖掉,使用 Symbol
值可以避免这一现象,比如 vue-router
中的 name
属性。
// a.js 文件
export const aRouter = {
path: ‘/index’,
name: Symbol(‘index’),
component: Index
},
// b.js 文件
export const bRouter = {
path: ‘/home’,
name: Symbol(‘index’), // 不重复
component: Home
},
// routes.js 文件
import { aRouter } from ‘./a.js’
import { bRouter } from ‘./b.js’
const routes = [
aRouter,
bRouter
]
两个不同文件使用了同样的 Symbol('index')
作为属性 name
的值,因 symbol
类型的唯一性,就避免了重复定义。
2、模拟类的私有方法。
const permission: symbol = Symbol(‘permission’)
class Auth {
// do something
}
}
这种情况通过类的实例是无法取到该方法,模拟类的私有方法。
但是,TypeScript
是可以使用 private
关键字的,所以这种方法可以在 JavaScript
中使用。
3、判断是否可以用 for...of
迭代。
if (Symbol.iterator in iterable) {
for(let n of iterable) {
console.log(n)
}
}
-
for...of
循环内部调用的是数据结构的Symbol.iterator
方法。 -
for...of
只能迭代可枚举属性。
4、Symbol.prototype.description
Symbol([description])
中可选的字符串即为这个 Symbol
的描述,如果想要获取这个描述:
const sym: symbol = Symbol(‘hello’)
console.log(sym); // Symbol(hello)
console.log(sym.toString()); // Symbol(hello)
console.log(sym.description); // hello
3.5、元组(Tuple
)
概念
相同类型元素组成成为数组,不同类型元素组成了元组(Tuple
)。
语法
声明一个由 string
和 number
构成的元组:
const list: [string, number] = [‘Sherlock’, 1887] // ok
const list1: [string, number] = [1887, ‘Sherlock’] // error
元组中规定的元素类型顺序必须是完全对照的,而且不能多、不能少,list1
中定义的第一个元素为 string
类型,不能赋值为 number
类型的数据。
当赋值或访问一个已知索引的元素时,会得到正确的类型:
const list: [string, number] = [‘Sherlock’, 1887]
//list[0] 是一个字符串类型,拥有 substr() 方法。
list[0].substr(1) // ok
//list[1] 是一个数字类型,没有 substr() 方法,所以报错
list[1].substr(1) // Property ‘substr’ does not exist on type ‘number’.
要注意元组的越界问题,虽然可以越界添加元素(不建议),但是不可越界访问:
const list: [string, number] = [‘Sherlock’, 1887]
//向一个声明了只有两个元素的元组继续添加元素,这种操作虽然可行,但是严重不建议!
list.push(‘hello world’)
//该元组只有两个元素,不可越界访问第三个元素
console.log(list) // ok [ ‘Sherlock’, 1887, ‘hello world’ ]
console.log(list[2]) // Tuple type ‘[string, number]’ of length ‘2’ has no element at index ‘2’
可选元素类型
元组类型允许在元素类型后缀一个 ?
来说明元素是可选的:
const list: [number, string?, boolean?]
list = [10, ‘Sherlock’, true]
list = [10, ‘Sherlock’]
list = [10]
可选元素必须在必选元素的后面,也就是如果一个元素后缀了 ?
号,其后的所有元素都要后缀 ?
号。
元组类型的 Rest
使用
元组可以作为参数传递给函数,函数的 Rest
形参可以定义为元组类型:
declare function rest(…args: [number, string, boolean]): void
等价于:
declare function rest(arg1: number, arg2: string, arg3: boolean): void
还可以这样:
const list: [number, …string[]] = [10, ‘a’, ‘b’, ‘c’]
const list1: [string, …number[]] = [‘a’, 1, 2, 3]
Rest
元素指定了元组类型是无限扩展的,可能有零个或多个具有数组元素类型的额外元素。
关键字 declare 表示声明作用
3.6、枚举(Enum
)
语法
使用枚举我们可以定义一些带名字的常量。TypeScript
支持数字的和基于字符串的枚举。枚举类型弥补了 JavaScript 的设计不足,很多语言都拥有枚举类型。
当我们需要一组相同主题下的数据时,枚举类型就很有用了。
enum Direction { Up, Down, Left, Right }
enum Months { Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec }
enum Size { big = ‘大’, medium = ‘中’, small = ‘小’ }
enum Agency { province = 1, city = 2, district = 3 }
代码中通过枚举类型分别声明了:不同的 方向
,一年内不同的 月份
,一个商品的不同 尺寸属性
,经销商的不同 级别
,这样的一组常量数据,是在一个相同主题下的不同表示。
数字枚举
声明一个枚举类型,如果没有赋值,它们的值默认为数字类型且从 0 开始累加:
enum Months {
Jan,
Feb,
Mar,
Apr
}
Months.Jan === 0 // true
Months.Feb === 1 // true
Months.Mar === 2 // true
Months.Apr === 3 // true
现实中月份是从 1 月开始的,那么只需要这样:
// 从第一个数字赋值,往后依次累加
enum Months {
Jan = 1, //从属性 Jan 被赋值为 1 开始,后续的属性值依次累加
Feb,
Mar,
Apr
}
Months.Jan === 1 // true
Months.Feb === 2 // true
Months.Mar === 3 // true
Months.Apr === 4 // true
字符串枚举
枚举类型的值为字符串类型:
enum TokenType {
ACCESS = ‘accessToken’,
REFRESH = ‘refreshToken’
}
// 两种不同的取值写法
console.log(TokenType.ACCESS === ‘accessToken’) // true
console.log(TokenType[‘REFRESH’] === ‘refreshToken’) // true
枚举的取值,有 TokenType.ACCESS
和 TokenType['ACCESS']
这两种不同的写法,效果是相同的。
数字类型和字符串类型可以混合使用,但是不建议:
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = “YES”,
}
计算常量成员
枚举类型的值可以是一个简单的计算表达式:
enum Calculate {
a,
b,
expired = 60 * 60 * 24,
length = ‘hello’.length,
plus = 'hello ’ + ‘world’
}
console.log(Calculate.expired) // 86400
console.log(Calculate.length) // 5
console.log(Calculate.plus) // hello world
-
计算结果必须为常量。
-
计算项必须放在最后。
反向映射
所谓的反向映射就是指枚举的取值,不但可以正向的 Months.Jan
这样取值,也可以反向的 Months[1]
这样取值。
enum Months {
Jan = 1,
Feb,
Mar,
Apr
}
将上面的代码进行编译,查看编译后的 JavaScript
代码:
‘use strict’
var Months;
(function (Months) {
Months[Months[‘Jan’] = 1] = ‘Jan’
Months[Months[‘Feb’] = 2] = ‘Feb’
Months[Months[‘Mar’] = 3] = ‘Mar’
Months[Months[‘Apr’] = 4] = ‘Apr’
})(Months || (Months = {}))
通过查看编译后的代码,可以得出:
console.log(Months.Mar === 3) // true
// 那么反过来能取到 Months[3] 的值吗?
console.log(Months[3]) // ‘Mar’
// 所以
console.log(Months.Mar === 3) // true
console.log(Months[3] === ‘Mar’) // true
-
字符串枚举成员不会生成反向映射。
-
枚举类型被编译成一个对象,它包含了正向映射(
name -> value
)和反向映射(value -> name
)。
const
枚举
在枚举上使用 const
修饰符:
enum Months {
Jan = 1,
Feb,
Mar,
Apr
}
const month = Months.Mar
查看一下编译后的内容:
‘use strict’
const month = 3 /* Mar */
发现枚举类型应该编译出的对象没有了,只剩下 month
常量。这就是使用 const
关键字声明枚举的作用。因为变量 month
已经使用过枚举类型,在编译阶段 TypeScript
就将枚举类型抹去,这也是性能提升的一种方案。
枚举合并
分开声明名称相同的枚举类型,会自动合并:
enum Months {
Jan = 1,
Feb,
Mar,
Apr
}
enum Months {
May = 5,
Jun
}
console.log(Months.Apr) // 4
console.log(Months.Jun) // 6
编译后的 JavaScript 代码:
‘use strict’
var Months;
(function (Months) {
Months[Months[‘Jan’] = 1] = ‘Jan’
Months[Months[‘Feb’] = 2] = ‘Feb’
Months[Months[‘Mar’] = 3] = ‘Mar’
Months[Months[‘Apr’] = 4] = ‘Apr’
})(Months || (Months = {}));
(function (Months) {
Months[Months[‘May’] = 5] = ‘May’
Months[Months[‘Jun’] = 6] = ‘Jun’
})(Months || (Months = {}))
console.log(Months.Apr) // 4
console.log(Months.Jun) // 6
3.7、Never
类型
概念
never
类型表示那些永不存在的值的类型。
never
类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是 never
的子类型或可以赋值给 never
类型(除了 never
本身之外)。 即使 any
也不可以赋值给 never
。
应用场景
一个抛出异常的函数表达式,其函数返回值类型为 never
:
function error(message:string): never {
throw new Error(message)
}
同样的,不会有返回值的函数表达式,其函数返回值类型也为 never
:
// 推断的返回值类型为 never
function fail(): never {
return error(“Something failed”)
}
不能取得值的地方:
interface Foo {
type: ‘foo’
}
interface Bar {
type: ‘bar’
}
type All = Foo | Bar
function handleValue(val: All) {
switch (val.type) {
case ‘foo’:
break
case ‘bar’:
break
default:
// 此处不能取值
const exhaustiveCheck: never = val
break
}
}
3.8、unknown
类型
any
无需事先执行任何类型的检查:
let value: any
value = true // OK
value = 10 // OK
value = “Hello World” // OK
value = [] // OK
value = {} // OK
value = Math.random // OK
value = null // OK
value = undefined // OK
value = new TypeError() // OK
value = Symbol(‘name’) // OK
value.foo.bar // OK
value.trim() // OK
value() // OK
new value() // OK
value[0][1] // OK
在许多情况下,这太宽松了。如果使用了 unknown
类型:
let value: unknown
value = true // OK
value = 10 // OK
value = “Hello World” // OK
value = [] // OK
value = {} // OK
value = Math.random // OK
value = null // OK
value = undefined // OK
value = new TypeError() // OK
value = Symbol(‘name’) // OK
所有对该 value
变量的分配都被认为是类型正确的。
可以尝试:
let value: unknown
let value1: unknown = value // OK
let value2: any = value // OK
let value3: boolean = value // Error
let value4: number = value // Error
let value5: string = value // Error
let value6: object = value // Error
let value7: any[] = value // Error
可以看到,该 unknown
类型只能分配给 any
类型和 unknown
类型本身。
继续尝试:
let value: unknown
value.foo.bar // Error
value.trim() // Error
value() // Error
new value() // Error
value[0][1] // Error
unknown
类型在被确定为某个类型之前,不能被进行诸如函数执行、实例化等操作,一定程度上对类型进行了保护。
在那些将取得任意值,但不知道具体类型的地方使用
unknown
,而非any
。
4.1、接口的概念
接口是对 JavaScript
本身的随意性进行约束,通过定义一个接口,约定了变量、类、函数等应该按照什么样的格式进行声明,实现多人合作的一致性。TypeScript
编译器依赖接口用于类型检查,最终编译为 JavaScript
后,接口将会被移除。
// 语法格式
interface DemoInterface {
}
4.2、应用场景
在声明一个对象、函数或者类时,先定义接口,确保其数据结构的一致性。
在多人协作时,定义接口尤为重要。
4.3、接口的好处
过去我们写 JavaScript
定义一个函数:
function getClothesInfo(clothes) {
console.log(clothes.price)
}
let myClothes = {
color: ‘black’,
size: ‘XL’,
price: 98
}
getClothesInfo(myClothes)
之前我们写 JavaScript
这样是很正常的,但同时你可能会遇到下面这些问题:
getClothesInfo() // Uncaught TypeError: Cannot read property ‘price’ of undefined
getClothesInfo({ color: ‘black’ }) // undefined
相信原因你也知道,JavaScript
是 弱类型
语言,并不会对传入的参数进行任何检测,错误在运行时才被发现。那么通过定义 接口
,在编译阶段甚至开发阶段就避免掉这类错误,接口将检查类型是否和某种结构做匹配。
下面通过接口的方式重写之前的例子:
interface Clothes {
color: string;
size: string;
price: number;
}
function getClothesInfo(clothes: Clothes) {
console.log(clothes.price)
}
let myClothes: Clothes = {
color: ‘black’,
size: ‘XL’,
price: 98
}
getClothesInfo(myClothes)
代码中,定义了一个接口 Clothes
,在传入的变量 clothes
中,它的类型为 Clothes
。这样,就约束了这个传入对象的 外形
与接口定义一致。只要传入的对象满足上面的类型约束,那么它就是被允许的。
-
定义接口要 首字母大写。
-
只需要关注值的 外形,并不像其他语言一样,定义接口是为了实现。
-
如果没有特殊声明,定义的变量比接口少了一些属性是不允许的,多一些属性也是不允许的,赋值的时候,变量的形状必须和接口的形状保持一致。
4.4、接口的属性
可选属性
接口中的属性不全是必需的。可选属性的含义是该属性在被变量定义时可以不存在。
// 语法
interface Clothes {
color?: string;
size: string;
price: number;
}
// 这里可以不定义属性 color
let myClothes: Clothes = {
size: ‘XL’,
price: 98
}
带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个 ? 符号。这时,仍不允许添加未定义的属性,如果引用了不存在的属性时 TS 将直接捕获错误。
只读属性
一些对象属性只能在对象刚刚创建的时候修改其值。你可以在属性名前用 readonly
来指定只读属性,比如价格是不能被修改的:
// 语法
interface Clothes {
color?: string;
size: string;
readonly price: number;
}
// 创建的时候给 price 赋值
let myClothes: Clothes = {
size: ‘XL’,
price: 98
}
// 不可修改
myClothes.price = 100
// error TS2540: Cannot assign to ‘price’ because it is a constant or a read-only property
TypeScript
可以通过 ReadonlyArray<T>
设置数组为只读,那么它的所有写方法都会失效。
let arr: ReadonlyArray = [1,2,3,4,5];
arr[0] = 6; // Index signature in type ‘readonly number[]’ only permits reading
最简单判断该用 readonly
还是 const
的方法是看要把它做为变量使用还是做为一个属性。做为 变量
使用的话用 const
,若做为 属性
则使用 readonly
。
任意属性
有时候我们希望接口允许有任意的属性,语法是用 []
将属性包裹起来:
// 语法
interface Clothes {
color?: string;
size: string;
readonly price: number;
}
// 任意属性 activity
let myClothes: Clothes = {
size: ‘XL’,
price: 98,
activity: ‘coupon’
}
这里的接口 Clothes
可以有任意数量的属性,并且只要它们不是 color size
和 price
,那么就无所谓它们的类型是什么。
案例:
使用 axios
库发起 HTTP
传输的时候,可以写入一个自定义的属性,就是因为源码中定义了一个任意属性:
this.$axios({
method: ‘put’,
url: ‘/cms/user’,
data: {
nickname: this.nickname,
},
showBackend: true,
})
4.5、函数类型
除了描述带有属性的普通对象外,接口也可以描述函数类型。
为了使接口表示函数类型,我们需要给接口定义一个调用签名。 它就像是一个只有 参数列表
和 返回值类型
的函数定义。
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string): boolean {
return source.search(subString) > -1;
}
对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配。你可以改变函数的参数名,只要保证函数参数的位置不变。函数的参数会被逐个进行检查:
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
// source => src, subString => sub
mySearch = function(src: string, sub: string): boolean {
return src.search(sub) > -1;
}
如果你不想指定类型,TypeScript
的类型系统会推断出参数类型,因为函数直接赋值给了 SearchFunc
类型变量。
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(src, sub) {
let result = src.search(sub);
return result > -1;
}
如果接口中的函数类型带有函数名,下面两种书写方式是等价的:
interface Calculate {
add(x: number, y: number): number
multiply: (x: number, y: number) => number
}
4.6、可索引类型
代码示例:
// 正常的js代码
let arr = [1, 2, 3, 4, 5]
let obj = {
brand: ‘hello’,
type: ‘education’
}
arr[0]
obj[‘brand’]
定义可索引类型接口:
interface ScenicInterface {
}
let arr: ScenicInterface = [‘西湖’, ‘华山’, ‘故宫’]
let favorite: string = arr[0]
示例中索引签名是 number类型
,返回值是字符串类型。
另外还有一种索引签名是 字符串类型
。我们可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。通过下面的例子理解这句话:
// 正确
interface Foo {
x: number;
y: number;
}
// 错误
interface Bar {
x: number;
y: string; // Error: y 属性必须为 number 类型
}
第 12 行,语法错误是因为当使用 number
来索引时,JavaScript
会将它转换成 string
然后再去索引对象。也就是说用 100
(一个number
)去索引等同于使用"100"(一个string
)去索引,因此两者需要保持一致。
4.7、类类型
我们希望类的实现必须遵循接口定义,那么可以使用 implements
关键字来确保兼容性。
这种类型的接口在传统面向对象语言中最为常见,比如 java
中接口就是这种类类型的接口。这种接口与抽象类比较相似,但是接口只能含有抽象方法和成员属性,实现类中必须实现接口中所有的抽象方法和成员属性。
interface AnimalInterface {
name: string;
}
class Dog implements AnimalInterface {
name: string;
constructor(name: string){
this.name = name
}
}
你也可以在接口中描述一个方法,在类里实现它:
interface AnimalInterface {
name: string
eat(m: number): string
}
class Dog implements AnimalInterface {
name: string;
constructor(name: string){
this.name = name
}
eat(m: number) {
return ${this.name}吃肉${m}分钟
}
}
接口描述了类的公共部分,而不是公共和私有两部分。 它不会帮你检查类是否具有某些私有成员。
4.8、继承接口
和类一样,接口也可以通过关键字 extents
相互继承。 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square = {} as Square;
// 继承了 Shape 的属性
square.color = “blue”;
square.sideLength = 10;
一个接口可以继承多个接口,创建出多个接口的合成接口。
interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
interface Square extends Shape, PenStroke {
sideLength: number;
}
let square = {} as Square;
square.color = “blue”;
square.sideLength = 10;
square.penWidth = 5.0;
4.9、混合类型
接口可以描述函数、对象的方法或者对象的属性。
有时希望一个对象同时具有上面提到多种类型,比如一个对象可以当做函数使用,同时又具有属性和方法。
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
function getCounter(): Counter {
let counter = function (start: number) { } as Counter;
counter.interval = 123;
counter.reset = function () { };
return counter;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
上面代码中,第 1 行,声明一个接口,如果只有 (start: number): string
一个成员,那么这个接口就是函数接口,同时还具有其他两个成员,可以用来描述对象的属性和方法,这样就构成了一个混合接口。
第 7 行,创建一个 getCounter()
函数,它的返回值是 Counter 类型的。
let counter = function (start: number) { } as Counter;
第 8 行,通过类型断言,将函数对象转换为 Counter
类型,转换后的对象不但实现了函数接口的描述,使之成为一个函数,还具有 interval
属性和 reset()
方法。断言成功的条件是,两个数据类型只要有一方可以赋值给另一方,这里函数类型数据不能赋值给接口类型的变量,因为它不具有 interval
属性和 reset()
方法。
5.1、类的概念
类描述了所创建的对象共同的属性和方法。通过 class
关键字声明一个类,主要包含以下模块:
-
属性
-
构造函数
-
方法
5.2、类的本质
JavaScript
中,生成实例对象可以通过构造函数的方式:
function Calculate (x, y) {
this.x = x
this.y = y
}
Calculate.prototype.add = function () {
return this.x + this.y
}
var calculate = new Calculate(1, 2)
console.log(calculate.add()) // 3
如果通过 class
关键字进行改写:
class Calculate {
// 类的属性
public x: number
public y: number
// 构造函数
constructor(x: number, y: number) {
this.x = x
this.y = y
}
// 类的方法
add () {
return this.x + this.y
}
}
const calculate = new Calculate(1, 2)
console.log(calculate.add()) // 3
console.log(typeof Calculate) // ‘function’
console.log(Calculate === Calculate.prototype.constructor) // true
最后一行,可以看出,类指向其构造函数本身,class
关键字可以看做是一个语法糖。
constructor()
方法是类的默认方法,通过 new
来生成对象实例时,自动调用该方法。换句话说,constructor()
方法默认返回实例对象 this
。
5.3、类的继承
基于类的程序设计中一种最基本的模式是允许使用继承来扩展现有的类,这样可以抽出公共部分让子类复用。
使用 extends
关键字来实现继承:
// 继承 JavaScript 内置的 Date 对象
class LinDate extends Date {
getFormattedDate() {
var months = [‘Jan’,‘Feb’,‘Mar’,‘Apr’,‘May’,‘Jun’,‘Jul’,‘Aug’,‘Sep’,‘Oct’,‘Nov’,‘Dec’];
return this.getDate() + “-” + months[this.getMonth()] + “-” + this.getFullYear();
}
}
const date = new LinDate()
console.log(date.getFullYear()); // 2020
console.log(date.getFormattedDate()) // 7-Jan-2020
LinDate
继承了 Date
的功能,可以使用父类 Date
的方法 getFullYear()
,也可以使用自身的方法 getFormattedDate()
。
子类在 constructor
内中使用 super()
方法调用父类的构造函数,在一般方法内使用 super.method()
执行父类的方法。
class Animal {
public name:string
constructor(name: string) {
this.name = name
}
move(distance: number = 0) {
console.log(${this.name} moved ${distance}m.
)
}
}
class Dog extends Animal {
constructor(name: string) {
// 调用父类的构造函数
super(name)
}
move(distance = 10) {
console.log(‘bark…’)
// 执行父类的方法
super.move(distance)
}
}
const dog: Animal = new Dog(‘Coco’)
dog.move() // Coco moved 10m.
5.4、访问修饰符
TypeScript
可以使用四种访问修饰符 public
、protected
、private
和 readonly
。
public
TypeScript
中,类的成员全部默认为 public
,当然你也可以显式的将一个成员标记为 public
,标记为 public
后,在程序类的外部可以访问。
class Calculate {
// 类的属性
public x: number
public y: number
// 构造函数
public constructor(x: number, y: number) {
this.x = x
this.y = y
}
public add () {
return this.x + this.y
}
}
protected
当成员被定义为 protected
后,只能被类的内部以及类的子类访问。
class Base {
//被定义成受保护的
protected baseUrl: string = ‘http://api.com/’
constructor() {}
protected request(method: string) {
//类的内部可以访问受保护的成员属性
const url = ${this.baseUrl}${method}
// TODO 封装基础的 http 请求
}
}
class Address extends Base {
get() {
//子类可以访问父类中受保护的属性和方法
return this.request(‘address’)
}
}
private
当类的成员被定义为 private
后,只能被类的内部访问。
class Mom {
private labour() {
return ‘baby is coming’
}
}
class Son extends Mom {
test () {
this.labour() // Error, Property ‘labour’ is private and only accessible within class ‘Mom’
}
}
在上面代码的第 9 行,父类中的 labour()
方法被定义为私有方法,只能在父类中被使用,子类中调用报错。
readonly
通过 readonly
关键字将属性设置为只读的。只读属性必须在声明时或构造函数里被初始化。
class Token {
readonly secret: string = ‘xjx*xh3GzW#3’
readonly expired: number
constructor (expired: number) {
this.expired = expired
}
}
const token = new Token(60 * 60 * 24)
token.expired = 60 * 60 * 2 // Error, expired 是只读的
上面代码中最后一行,因 Token
类的属性expired
被设置为只读属性,不可被修改。
5.5、静态方法
通过 static
关键字来创建类的静态成员,这些属性存在于类本身上面而不是类的实例上。
class User {
static getInformation () {
return ‘This guy is too lazy to write anything.’
}
}
User.getInformation() // OK
const user = new User()
user.getInformation() // Error 实例中无此方法
getInformation()
方法被定义为静态方法,只存在于类本身上,类的实例无法访问。
静态方法调用同一个类中的其他静态方法,可使用 this
关键字。
class StaticMethodCall {
static staticMethod() {
return ‘Static method has been called’
}
static anotherStaticMethod() {
return this.staticMethod() + ’ from another static method’
}
}
静态方法中的 this
指向类本身,而静态方法也存在于类本身,所以可以在静态方法中用 this
访问在同一类中的其他静态方法。
非静态方法中,不能直接使用 this
关键字来访问静态方法。而要用类本身或者构造函数的属性来调用该方法:
class StaticMethodCall {
constructor() {
// 类本身调用
console.log(StaticMethodCall.staticMethod())
// 构造函数的属性调用
console.log(this.constructor.staticMethod())
}
static staticMethod() {
return ‘static method has been called.’
}
}
类指向其构造函数本身,在非静态方法中,this.constructor === StaticMethodCall
为 true
, 也就是说这两种写法等价。
5.6、抽象类
抽象类作为其它派生类的基类使用,它们一般不会直接被实例化,不同于接口,抽象类可以包含成员的实现细节。
abstract
关键字是用于定义抽象类和在抽象类内部定义抽象方法。
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log(‘roaming the earch…’);
}
}
const animal = new Animal() // Error, 无法创建抽象类实例
通常我们需要创建子类继承抽象类,将抽象类中的抽象方法一一实现,这样在大型项目中可以很好的约束子类的实现。
class Dog extends Animal {
makeSound() {
console.log(‘bark bark bark…’)
}
}
const dog = new Dog()
dog.makeSound() // bark bark bark…
dog.move() // roaming the earch…
5.7、把类当做接口使用
类也可以作为接口来使用,这在项目中是很常见的。
class Pizza {
constructor(public name: string, public toppings: string[]) {}
}
class PizzaMaker {
// 把 Pizza 类当做接口
static create(event: Pizza) {
return new Pizza(event.name, event.toppings)
}
}
const pizza = PizzaMaker.create({
name: ‘Cheese and nut pizza’,
toppings: [‘pasta’, ‘eggs’, ‘milk’, ‘cheese’]
})
因为接口和类都定义了对象的结构,在某些情况下可以互换使用。如果你需要创建一个可以自定义参数的实例,同时也可以进行类型检查,把类当做接口使用不失为一个很好的方法。
这就是 TypeScript
的强大功能,而且非常灵活,拥有全面的面向对象设计和通用的类型检查。
6.1、函数的概念
在 JavaScript
中,函数是头等(first-class
)对象,因为它们可以像任何其他对象一样具有属性和方法。在 JavaScript
中,每个函数都是一个 Function
对象。
TypeScript
又为 JavaScript
函数添加了一些额外的功能,让我们可以更容易地使用:
-
函数类型
-
可选参数
-
默认参数
-
剩余参数
-
函数重载
6.2、函数类型
在 TypeScript
中编写函数,需要给形参和返回值指定类型:
const add = function(x: number, y: number): string {
return (x + y).toString()
}
参数 x
和 y
都是 number
类型,两个参数相加后将其类型转换为 string
, 所以整个函数的返回值为 string
类型。
上面的代码只是对 =
等号右侧的匿名函数进行了类型定义,等号左侧的 add
同样可以添加类型:
const add: (x: number, y: number) => string = function(x: number, y: number): string {
return (x + y).toString()
}
可以看到,等号左侧的类型定义由两部分组成:参数类型和返回值类型,通过 =>
符号来连接。
这里要注意:函数类型的 =>
和 箭头函数的 =>
是不同的含义。
通过箭头函数改写一下刚才写的函数:
const add = (x: number, y: number): string => (x + y).toString()
等号左右两侧书写完整:
// 只要参数位置及类型不变,变量名称可以自己定义,比如把两个参数定位为 a b
const add: (a: number, b: number) => string = (x: number, y: number): string => (x + y).toString()
5.3、函数的参数
参数个数保持一致
TypeScript
中每个函数参数都是必须的。 这不是指不能传递 null
或 undefined
作为参数,而是说编译器会检查用户是否为每个参数都传入了值。简短地说,传递给一个函数的参数个数必须与函数期望的参数个数一致。
const fullName = (firstName: string, lastName: string): string => ${firstName}${lastName}
let result1 = fullName(‘Sherlock’, ‘Holmes’)
let result2 = fullName(‘Sherlock’, ‘Holmes’, ‘character’) // Error, Expected 2 arguments, but got 3
let result3 = fullName(‘Sherlock’) // Error, Expected 2 arguments, but got 1
可选参数
在 JavaScript
中每个参数都是可选的,可传可不传。没传参的时候,它的值就是 undefined
。 而在 TypeScript
里我们可以在参数名旁使用 ?
实现可选参数的功能,可选参数必须跟在必须参数后面。
const fullName = (firstName: string, lastName?: string): string => ${firstName}${lastName}
let result1 = fullName(‘Sherlock’, ‘Holmes’)
let result2 = fullName(‘Sherlock’, ‘Holmes’, ‘character’) // Error, Expected 1-2 arguments, but got 3
let result3 = fullName(‘Sherlock’) // OK
默认参数
参数可以取默认值,上面介绍的可选参数必须跟在必须参数后面,而带默认值的参数不需要放在必须参数的后面,可随意调整位置:
const token = (expired = 60*60, secret: string): void => {}
// 或
const token1 = (secret: string, expired = 60*60 ): void => {}
剩余参数
有的时候,函数的参数个数是不确定的,可能传入未知个数,这时没有关系,有一种方法可以解决这个问题。
通过 rest 参数
(形式为 ...变量名
)来获取函数的剩余参数,这样就不需要使用 arguments
对象了。
function assert(ok: boolean, …args: string[]): void {
if (!ok) {
throw new Error(args.join(’ '));
}
}
assert(false, ‘上传文件过大’, ‘只能上传jpg格式’)
注意
rest 参数
只能是最后一个参数。
this
参数
JavaScript
里,this
的值在函数被调用的时候才会被指定,但是这个 this
到底指的是什么还是需要花点时间弄清楚。
默认情况下,tsconfig.json
中,编译选项 compilerOptions
的属性 noImplicitThis
为 false
,我们在一个对象中使用的 this
时,它的类型是 any
类型。
let triangle = {
a: 10,
b: 15,
c: 20,
area: function () {
return () => {
// this 为 any 类型
const p = (this.a + this.b + this.c) / 2
return Math.sqrt(p * (p - this.a) * (p - this.b) *(p - this.c))
}
}
}
const myArea = triangle.area()
console.log(myArea())
在实际工作中 any
类型是非常危险的,我们可以添加任意属性到 any
类型的参数上,比如将 const p = (this.a + this.b + this.c) / 2
这句改为 const p = (this.d + this.d + this.d) / 2
也不会报错,这很容易造成不必要的问题。
所以我们应该明确 this
的指向,下面介绍两种方法:
第一种,在 tsconfig.json
中,将编译选项 compilerOptions
的属性 noImplicitThis
设置为 true
,TypeScript
编译器就会帮你进行正确的类型推断:
let triangle = {
a: 10,
b: 15,
c: 20,
area: function () {
return () => {
const p = (this.a + this.b + this.c) / 2
return Math.sqrt(p * (p - this.a) * (p - this.b) *(p - this.c))
}
}
}
const myArea = triangle.area()
console.log(myArea())
将 noImplicitThis
设置为 true
以后,把鼠标放在第 7 行的 this
上,可以看到:
this: {
a: number;
b: number;
c: number;
area: () => () => number;
}
这时,TypeScript
编译器就能准确的知道了 this
的类型,如果取不存在于 this
属性中的 d
,将会报错 Property 'd' does not exist on type '{ a: number; b: number; c: number; area: () => () => any; }'
除了这种方法,我们还可以通过 this 参数
这种形式来解决 this
为 any
类型这一问题。提供一个显式的 this
参数,它出现在参数列表的最前面:
// 语法
function f(this: void) {
}
改造刚才的例子:
interface Triangle {
a: number;
b: number;
c: number;
area(this: Triangle): () => number;
}
let triangle: Triangle = {
a: 10,
b: 15,
c: 20,
area: function (this: Triangle) {
return () => {
const p = (this.a + this.b + this.c) / 2
return Math.sqrt(p * (p - this.a) * (p - this.b) *(p - this.c))
}
}
}
const myArea = triangle.area()
console.log(myArea())
我们声明了一个接口 Triangle
,其中的函数类型显式的传入了 this
参数,这个参数的类型为 Triangle
类型(第 5 行):
area(this: Triangle): () => number;
此时,在第 14 行,this
指向 Triangle
,就可以进行正确的类型判断,如果取未定义参数,编译器将直接报错。
6.4、函数重载
函数重载是指函数根据传入不同的参数,返回不同类型的数据。
它的意义在于让你清晰的知道传入不同的参数得到不同的结果,如果传入的参数不同,但是得到相同类型的数据,那就不需要使用函数重载。
比如面试中常考的字符反转问题,这里就不考虑负数情况了,只是为了演示函数重载:
function reverse(target: string | number) {
if (typeof target === ‘string’) {
return target.split(‘’).reverse().join(‘’)
}
if (typeof target === ‘number’) {
return +[…target.toString()].reverse().join(‘’)
}
}
console.log(reverse(‘hello’)) // olleh
console.log(reverse(23874800)) // 847832
编译器并不知道入参是什么类型的,返回值类型也不能确定。这时可以为同一个函数提供多个函数类型定义来进行函数重载。
(通过 --downlevelIteration
编译选项增加对生成器和迭代器协议的支持)
function reverse(x: string): string
function reverse(x: number): number
function reverse(target: string | number) {
if (typeof target === ‘string’) {
return target.split(‘’).reverse().join(‘’)
}
if (typeof target === ‘number’) {
return +[…target.toString()].reverse().join(‘’)
}
}
console.log(reverse(‘hello’)) // olleh
console.log(reverse(23874800)) // 847832
因为这个反转函数在传入字符串类型的时候返回字符串类型,传入数字类型的时候返回数字类型,所以在前两行进行了两次函数类型定义。在函数执行时,根据传入的参数类型不同,进行不同的计算。
为了让编译器能够选择正确的检查类型,它会从重载列表的第一个开始匹配。因此,在定义重载时,一定要把最精确的定义放在最前面。
6.5、使用函数时的注意事项
-
如果一个函数没有使用
return
语句,则它默认返回undefined
。 -
调用函数时,传递给函数的值被称为函数的
实参
(值传递),对应位置的函数参数被称为形参
。 -
在函数执行时,
this
关键字并不会指向正在运行的函数本身,而是指向调用函数的对象
。 -
arguments
对象是所有(非箭头)函数中都可用的局部变量
。你可以使用arguments
对象在函数中引用函数的参数。
7.1、泛型的概念
泛型在传统的面向对象语言中极为常见,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。
通俗来讲:泛型是指在定义函数、接口或者类时,未指定其参数类型,只有在运行时传入才能确定。那么此时的参数类型就是一个变量,通常用大写字母 T
来表示,当然你也可以使用其他字符,如:U
、K
等。
语法:在函数名、接口名或者类名添加后缀 <T>
:
function generic() {}
interface Generic {}
class Generic {}
之所以使用泛型,是因为它帮助我们为不同类型的输入,复用相同的代码。
比如写一个最简单的函数,这个函数会返回任何传入它的值。如果传入的是 number
类型:
function identity(arg: number): number {
return arg
}
如果传入的是 string 类型:
function identity(arg: string): string {
return arg
}
通过泛型,可以把两个函数统一起来:
function identity(arg: T): T {
return arg
}
需要注意的是,泛型函数的返回值类型是根据你的业务需求决定,并非一定要返回泛型类型 T:
function identity(arg: T): string {
return String(arg)
}
入参的类型是未知的,但是通过 String 转换,返回字符串类型。
7.2、多个类型参数
泛型函数可以定义多个类型参数:
function extend<T, U>(first: T, second: U): T & U {
for(const key in second) {
(first as T & U)[key] = second[key] as any
}
return first as T & U
}
这个函数用来合并两个对象,具体实现暂且不去管它,这里只需要关注泛型多个类型参数的使用方式,其语法为通过逗号分隔 <T, U, K>
。
7.3、泛型参数默认类型
函数参数可以定义默认值,泛型参数同样可以定义默认类型:
function min<T = number>(arr:T[]): T{
let min = arr[0]
arr.forEach((value)=>{
if(value < min) {
min = value
}
})
return min
}
console.log(min([20, 6, 8n])) // 6
同样的不用去关注这个最小数函数的具体实现,要知道默认参数语法为 <T = 默认类型>
。
7.4、泛型类型与泛型接口
先看一下函数类型:
const add: (x: number, y: number) => string = function(x: number, y: number): string {
return (x + y).toString()
}
等号左侧的 (x: number, y: number) => string
为函数类型。
再看下泛型类型:
function identity(arg: T): T {
return arg
}
let myIdentity: (arg: T) => T = identity
同样的等号左侧的 <T>(arg: T) => T
即为泛型类型,它还有另一种_带有调用签名的对象字面量_书写方式:{ <T>(arg: T): T }
:
function identity(arg: T): T {
return arg
}
let myIdentity: { (arg: T): T } = identity
这就引导我们去写第一个泛型接口了。把上面例子里的对象字面量拿出来作为一个接口:
interface GenericIdentityFn {
(arg: T): T
}
function identity(arg: T): T {
return arg
}
let myIdentity: GenericIdentityFn = identity
进一步,把泛型参数当作整个接口的一个参数,我们可以把泛型参数提前到接口名上。这样我们就能清楚的知道使用的具体是哪个泛型类型:
interface GenericIdentityFn {
(arg: T): T
}
function identity(arg: T): T {
return arg
}
let myIdentity: GenericIdentityFn = identity
注意,在使用泛型接口时,需要传入一个类型参数来指定泛型类型。示例中传入了 number
类型,这就锁定了之后代码里使用的类型。
7.5、泛类型
使用泛型是因为可以复用不同类型的代码。下面用一个最小堆算法举例说明泛型类的使用:
class MinClass {
public list: number[] = []
add(num: number) {
this.list.push(num)
}
min(): number {
let minNum = this.list[0]
for (let i = 0; i < this.list.length; i++) {
if (minNum > this.list[i]) {
minNum = this.list[i]
}
}
return minNum
}
}
示例中我们实现了一个查找 number 类型的最小堆类,但我们的最小堆还需要支持字符串类型,此时就需要泛型的帮助了:
// 类名后加上
class MinClass {
public list: T[] = []
add(num: T) {
this.list.push(num)
}
min(): T {
let minNum = this.list[0]
for (let i = 0; i < this.list.length; i++) {
if (minNum > this.list[i]) {
minNum = this.list[i]
}
}
return minNum
}
}
let m = new MinClass()
m.add(‘hello’)
m.add(‘world’)
m.add(‘generic’)
console.log(m.min()) // generic
第 2 行,在声明 类 MinClass
的后面后加上了 <T>
,这样就声明了泛型参数 T
,作为一个变量可以是字符串类型,也可以是数字类型。
7.6、泛型约束
语法:通过 extends
关键字来实现泛型约束。
如果我们很明确传入的泛型参数是什么类型,或者明确想要操作的某类型的值具有什么属性,那么就需要对泛型进行约束。通过两个例子来说明:
interface User {
username: string
}
function info(user: T): string {
return 'hello ’ + user.username
}
示例中,第 5 行,我们约束了入参 user
必须包含 username
属性,否则在编译阶段就会报错。
下面再看另外一个例子:
type Args = number | string
class MinClass {}
const m = new MinClass() // Error, 必须是 number | string 类型
在上面代码中,第 3 行,约束了泛型参数 T
继承自类型 Args
,而类型 Args
是一个由 number
和 string
组成的联合类型。
第 5 行,泛型参数只能是 number
和 string
中的一种,传入 boolean
类型是错误的。
7.7、多重类型泛型约束
通过 <T extends Interface1 & Interface2>
这种语法来实现多重类型的泛型约束:
interface Sentence {
title: string,
content: string
}
interface Music {
url: string
}
class Classic<T extends Sentence & Music> {
private prop: T
constructor(arg: T) {
this.prop = arg
}
info() {
return {
url: this.prop.url,
title: this.prop.title,
content: this.prop.content
}
}
}
在代码中,第 10 行,约束了泛型参数 T
需继承自交叉类型(后续有单节介绍) Sentence & Music
,这样就能访问两个接口类型的参数。
8.1、字面量类型
在计算机科学中,字面量(literal
)是用于表达源代码中一个固定值的表示法(notation
)。通俗的讲,字面量也可以叫直接量,就是你看见什么,它就是什么。
字符串类型,其实是一个集合类型,所有的字符串集合在一起构成了 string
类型。而字符串字面量类型就直接多了,你定义为 'hello'
,那这个变量的类型就是 'hello'
类型。
字符串字面量类型
字符串字面量类型允许你指定字符串必须的固定值。
let protagonist: ‘Sherlock’
protagonist = ‘Sherlock’
protagonist = ‘Watson’ // Error, Type ‘“Watson”’ is not assignable to type ‘“Sherlock”’
变量 protagonist
被声明为 'Sherlock'
字面量类型,就只能赋值为 'Sherlock'
。
type Easing = ‘ease-in’ | ‘ease-out’ | ‘ease-in-out’
class UIElement {
animate(dx: number, dy: number, easing: Easing) {
if (easing === ‘ease-in’) {}
else if (easing === ‘ease-out’) {}
else if (easing === ‘ease-in-out’) {}
else {
// Error, 不应该传递 null 或 undefined
}
}
}
let button = new UIElement()
button.animate(0, 0, ‘ease-in’)
button.animate(0, 0, ‘uneasy’) // Error, ‘uneasy’ 不被允许
在上面代码中,第 1 行,通过类型别名,声明了类型 Easing
为 'ease-in' | 'ease-out' | 'ease-in-out'
这样三个字符串字面量构成的联合类型。
第 4 行,你只能从三种允许的字符中选择其一来做为参数传递,传入其它值则会产生错误。
字符串字面量类型还可以用于区分函数重载:
function createElement(tagName: ‘img’): HTMLImageElement
function createElement(tagName: ‘input’): HTMLInputElement
function createElement(tagName: string): Element {}
如果参数 tagName
为 'img'
类型,返回值类型为 HTMLImageElement
; 如果参数 tagName
为 'input'
类型,返回值类型为 HTMLInputElement
。
布尔字面量类型
声明布尔字面量类型,注意这里是 :
不是 =
。 =
等号是变量赋值,:
表示声明的类型。
let success: true
let fail: false
let value: true | false
接口的返回值,会有正确返回和异常两种情况,这两种情况要有不同的数据返回格式:
type Result = { success: true, code: number, object: object } | { success: false, code: number, errMsg: string }
let res: Result = { success: false, code: 90001, errMsg: ‘该二维码已使用’ }
if (!res.success) {
res.errMsg // OK
res.object // Error, Property ‘object’ does not exist on type '{ success: false; code: number; errMsg: string; }
}
类型别名 Result
是一个由两个对象组成的联合类型,都有一个共同的 success
属性,这个属性的类型就是布尔字面量类型。
数字字面量类型
TypeScript
还具有数字字面量类型。
比如骰子只有六种点数:
let die: 1 | 2 | 3 | 4 | 5 | 6
die = 9 // Error
8.2、类型推断
类型推断的含义是不需要指定变量类型或函数的返回值类型,TypeScript
可以根据一些简单的规则推断其的类型。
基础类型推断
基础的类型推断发生在 初始化变量,设置默认参数和决定返回值时。
初始化变量例子:
let x = 3 // let x: number
let y = ‘hello world’ // let y: string
let z // let z: any
变量 x
的类型被推断为数字,变量 y
的类型被推断为字符串。如果定义时没有赋值,将被推断为 any
类型。
设置默认参数和决定返回值时的例子:
// 返回值推断为 number
function add(a:number, b:10) {
return a + b
}
const obj = {
a: 10,
b: ‘hello world’
}
obj.b = 15 // Error,Type ‘15’ is not assignable to type ‘string’
在上面代码中,第 1 行,参数 b 有默认值 10,被推断为 number
类型。
第 2 行,两个 number
类型相加,函数 add()
返回值被推断为 number
类型。
最后一行,obj
的类型被推断为 {a: number, b: string}
,所以属性 b 不能被赋值为数字。
const obj = {
protagonist: ‘Sherlock’,
gender: ‘male’
}
let { protagonist } = obj
最佳通用类型推断
当需要从多个元素类型推断出一个类型时,TypeScript
会尽可能推断出一个兼容所有类型的通用类型。
比如声明一个数组:
let x = [1, ‘hello’, null]
为了推断 x
的类型,必须考虑所有的元素类型。这里有三种元素类型 number
、string
和 null
,此时数组被推断为 let x: (string | number | null)[]
联合类型。
是否兼容
null
类型可以通过tsconfig.json
文件中属性strictNullChecks
的值设置为true
或false
来决定。
上下文类型推断
前面两种都是根据从右向左流动进行类型推断,上下文类型推断则是从左向右的类型推断。
例如定义一个 Animal
的类作为接口使用:
class Animal {
public species: string | undefined
public weight: number | undefined
}
const simba: Animal = {
species: ‘lion’,
speak: true // Error, ‘speak’ does not exist in type ‘Animal’
}
在上面代码中,第 6 行,将 Animal 类型
显示的赋值给 变量 simba
,Animal 类型
没有 speak 属性
,所以不可赋值。
8.3、类型断言
TypeScript
允许你覆盖它的推断,毕竟作为开发者你比编译器更了解你写的代码。
类型断言主要用于当 TypeScript
推断出来类型并不满足你的需求,你需要手动指定一个类型。
关键字 as
当你把 JavaScript
代码迁移到 TypeScript
时,一个常见的问题:
const user = {}
user.nickname = ‘Evan’ // Error, Property ‘nickname’ does not exist on type ‘{}’
user.admin = true // Error, Property ‘admin’ does not exist on type ‘{}’
编译器推断 const user: {}
,这是一个没有属性的对象,所以你不能对其添加属性。
此时可以使用类型断言(as
关键字)覆盖其类型推断:
interface User {
nickname: string,
admin: boolean,
groups: number[]
}
const user = {} as User
user.nickname = ‘Evan’
user.admin = true
user.groups = [2, 6]
在上面代码中,第 7 行,这里通过 as
关键字进行类型断言,将变量 user
的类型覆盖为 User
类型。但是请注意,类型断言不要滥用,除非你完全明白你在干什么。
首尾标签
类型断言还可以通过标签 <>
来实现:
interface User {
nickname: string,
admin: boolean,
groups: number[]
}
const user = {} // User类型
user.nickname = ‘Evan’
user.admin = true
user.groups = [2, 6]
在上面代码中,第 7 行,使用 <User>{}
这种标签形式,将变量 user
强制断言为 User
类型。
但是,当你在使用 JSX
语法时,会跟标签 <>
形式的类型断言混淆:
let nickname = Evan // 这里的 User 指向一个 component
所以,建议统一使用 as type
这种语法来为类型断言。
非空断言
如果编译器不能够去除 null
或 undefined
,可以使用非空断言 !
手动去除。
function fixed(name: string | null): string {
function postfix(epithet: string) {
return name!.charAt(0) + '. the ’ + epithet; // name 被断言为非空
}
name = name || “Bob”
return postfix(“great”)
}
在上面代码中,第 2 行,postfix()
是一个嵌套函数,因为编译器无法去除嵌套函数的 null
(除非是立即调用的函数表达式),所以 TypeScript
推断第 3 行的 name
可能为空。
第 5 行,而 name = name || "Bob"
这行代码已经明确了 name
不为空,所以可以直接给 name
断言为非空(第 3 行)。
双重断言
双重断言极少有应用场景,只需要知道有这种操作即可:
interface User {
nickname: string,
admin: boolean,
group: number[]
}
const user = ‘Evan’ as any as User
在上面代码中,最后一行,使用 as
关键字进行了两次断言,最终变量 user
被强制转化为 User
类型。
8.4、类型保护
类型保护是指缩小类型的范围,在一定的块级作用域内由编译器推导其类型,提示并规避不合法的操作。
typeof
通过 typeof
运算符判断变量类型,下面看一个之前介绍函数重载时的例子:
function reverse(target: string | number) {
if (typeof target === ‘string’) {
target.toFixed(2) // Error,在这个代码块中,target 是 string 类型,没有 toFixed 方法
return target.split(‘’).reverse().join(‘’)
}
if (typeof target === ‘number’) {
target.toFixed(2) // OK
return +[…target.toString()].reverse().join(‘’)
}
target.forEach(element => {}) // Error,在这个代码块中,target 是 string 或 number 类型,没有 forEach 方法
}
在上面代码中,第 2 行,通过 typeof
关键字,将这个代码块中变量 target
的类型限定为 string
类型。
第 6 行,通过 typeof
关键字,将这个代码块中变量 target
的类型限定为 number
类型。
第 11 行,因没有限定,在这个代码块中,变量 target
是 string
或 number
类型,没有 forEach
方法,所以报错。
instanceof
instanceof
与 typeof
类似,区别在于 typeof
判断基础类型,instanceof
判断是否为某个对象的实例:
React
-
介绍一下react
-
React单项数据流
-
react生命周期函数和react组件的生命周期
-
react和Vue的原理,区别,亮点,作用
-
reactJs的组件交流
-
有了解过react的虚拟DOM吗,虚拟DOM是怎么对比的呢
-
项目里用到了react,为什么要选择react,react有哪些好处
-
怎么获取真正的dom
-
选择react的原因
-
react的生命周期函数
-
setState之后的流程
-
react高阶组件知道吗?
-
React的jsx,函数式编程
-
react的组件是通过什么去判断是否刷新的
-
如何配置React-Router
-
路由的动态加载模块
-
Redux中间件是什么东西,接受几个参数
-
redux请求中间件如何处理并发
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
声明布尔字面量类型,注意这里是 :
不是 =
。 =
等号是变量赋值,:
表示声明的类型。
let success: true
let fail: false
let value: true | false
接口的返回值,会有正确返回和异常两种情况,这两种情况要有不同的数据返回格式:
type Result = { success: true, code: number, object: object } | { success: false, code: number, errMsg: string }
let res: Result = { success: false, code: 90001, errMsg: ‘该二维码已使用’ }
if (!res.success) {
res.errMsg // OK
res.object // Error, Property ‘object’ does not exist on type '{ success: false; code: number; errMsg: string; }
}
类型别名 Result
是一个由两个对象组成的联合类型,都有一个共同的 success
属性,这个属性的类型就是布尔字面量类型。
数字字面量类型
TypeScript
还具有数字字面量类型。
比如骰子只有六种点数:
let die: 1 | 2 | 3 | 4 | 5 | 6
die = 9 // Error
8.2、类型推断
类型推断的含义是不需要指定变量类型或函数的返回值类型,TypeScript
可以根据一些简单的规则推断其的类型。
基础类型推断
基础的类型推断发生在 初始化变量,设置默认参数和决定返回值时。
初始化变量例子:
let x = 3 // let x: number
let y = ‘hello world’ // let y: string
let z // let z: any
变量 x
的类型被推断为数字,变量 y
的类型被推断为字符串。如果定义时没有赋值,将被推断为 any
类型。
设置默认参数和决定返回值时的例子:
// 返回值推断为 number
function add(a:number, b:10) {
return a + b
}
const obj = {
a: 10,
b: ‘hello world’
}
obj.b = 15 // Error,Type ‘15’ is not assignable to type ‘string’
在上面代码中,第 1 行,参数 b 有默认值 10,被推断为 number
类型。
第 2 行,两个 number
类型相加,函数 add()
返回值被推断为 number
类型。
最后一行,obj
的类型被推断为 {a: number, b: string}
,所以属性 b 不能被赋值为数字。
const obj = {
protagonist: ‘Sherlock’,
gender: ‘male’
}
let { protagonist } = obj
最佳通用类型推断
当需要从多个元素类型推断出一个类型时,TypeScript
会尽可能推断出一个兼容所有类型的通用类型。
比如声明一个数组:
let x = [1, ‘hello’, null]
为了推断 x
的类型,必须考虑所有的元素类型。这里有三种元素类型 number
、string
和 null
,此时数组被推断为 let x: (string | number | null)[]
联合类型。
是否兼容
null
类型可以通过tsconfig.json
文件中属性strictNullChecks
的值设置为true
或false
来决定。
上下文类型推断
前面两种都是根据从右向左流动进行类型推断,上下文类型推断则是从左向右的类型推断。
例如定义一个 Animal
的类作为接口使用:
class Animal {
public species: string | undefined
public weight: number | undefined
}
const simba: Animal = {
species: ‘lion’,
speak: true // Error, ‘speak’ does not exist in type ‘Animal’
}
在上面代码中,第 6 行,将 Animal 类型
显示的赋值给 变量 simba
,Animal 类型
没有 speak 属性
,所以不可赋值。
8.3、类型断言
TypeScript
允许你覆盖它的推断,毕竟作为开发者你比编译器更了解你写的代码。
类型断言主要用于当 TypeScript
推断出来类型并不满足你的需求,你需要手动指定一个类型。
关键字 as
当你把 JavaScript
代码迁移到 TypeScript
时,一个常见的问题:
const user = {}
user.nickname = ‘Evan’ // Error, Property ‘nickname’ does not exist on type ‘{}’
user.admin = true // Error, Property ‘admin’ does not exist on type ‘{}’
编译器推断 const user: {}
,这是一个没有属性的对象,所以你不能对其添加属性。
此时可以使用类型断言(as
关键字)覆盖其类型推断:
interface User {
nickname: string,
admin: boolean,
groups: number[]
}
const user = {} as User
user.nickname = ‘Evan’
user.admin = true
user.groups = [2, 6]
在上面代码中,第 7 行,这里通过 as
关键字进行类型断言,将变量 user
的类型覆盖为 User
类型。但是请注意,类型断言不要滥用,除非你完全明白你在干什么。
首尾标签
类型断言还可以通过标签 <>
来实现:
interface User {
nickname: string,
admin: boolean,
groups: number[]
}
const user = {} // User类型
user.nickname = ‘Evan’
user.admin = true
user.groups = [2, 6]
在上面代码中,第 7 行,使用 <User>{}
这种标签形式,将变量 user
强制断言为 User
类型。
但是,当你在使用 JSX
语法时,会跟标签 <>
形式的类型断言混淆:
let nickname = Evan // 这里的 User 指向一个 component
所以,建议统一使用 as type
这种语法来为类型断言。
非空断言
如果编译器不能够去除 null
或 undefined
,可以使用非空断言 !
手动去除。
function fixed(name: string | null): string {
function postfix(epithet: string) {
return name!.charAt(0) + '. the ’ + epithet; // name 被断言为非空
}
name = name || “Bob”
return postfix(“great”)
}
在上面代码中,第 2 行,postfix()
是一个嵌套函数,因为编译器无法去除嵌套函数的 null
(除非是立即调用的函数表达式),所以 TypeScript
推断第 3 行的 name
可能为空。
第 5 行,而 name = name || "Bob"
这行代码已经明确了 name
不为空,所以可以直接给 name
断言为非空(第 3 行)。
双重断言
双重断言极少有应用场景,只需要知道有这种操作即可:
interface User {
nickname: string,
admin: boolean,
group: number[]
}
const user = ‘Evan’ as any as User
在上面代码中,最后一行,使用 as
关键字进行了两次断言,最终变量 user
被强制转化为 User
类型。
8.4、类型保护
类型保护是指缩小类型的范围,在一定的块级作用域内由编译器推导其类型,提示并规避不合法的操作。
typeof
通过 typeof
运算符判断变量类型,下面看一个之前介绍函数重载时的例子:
function reverse(target: string | number) {
if (typeof target === ‘string’) {
target.toFixed(2) // Error,在这个代码块中,target 是 string 类型,没有 toFixed 方法
return target.split(‘’).reverse().join(‘’)
}
if (typeof target === ‘number’) {
target.toFixed(2) // OK
return +[…target.toString()].reverse().join(‘’)
}
target.forEach(element => {}) // Error,在这个代码块中,target 是 string 或 number 类型,没有 forEach 方法
}
在上面代码中,第 2 行,通过 typeof
关键字,将这个代码块中变量 target
的类型限定为 string
类型。
第 6 行,通过 typeof
关键字,将这个代码块中变量 target
的类型限定为 number
类型。
第 11 行,因没有限定,在这个代码块中,变量 target
是 string
或 number
类型,没有 forEach
方法,所以报错。
instanceof
instanceof
与 typeof
类似,区别在于 typeof
判断基础类型,instanceof
判断是否为某个对象的实例:
React
-
介绍一下react
-
React单项数据流
-
react生命周期函数和react组件的生命周期
-
react和Vue的原理,区别,亮点,作用
-
reactJs的组件交流
-
有了解过react的虚拟DOM吗,虚拟DOM是怎么对比的呢
-
项目里用到了react,为什么要选择react,react有哪些好处
-
怎么获取真正的dom
-
选择react的原因
-
react的生命周期函数
-
setState之后的流程
-
react高阶组件知道吗?
-
React的jsx,函数式编程
-
react的组件是通过什么去判断是否刷新的
-
如何配置React-Router
-
路由的动态加载模块
-
Redux中间件是什么东西,接受几个参数
-
redux请求中间件如何处理并发