一、TypeScript简介
TypeScript(下称TS) 是 JavaScript 的超集,TS其实就是类型化的 JavaScript(下称JS),它不仅支持 JS的所有特性,还在 JS的基础上添加了静态类型注解扩展。
二、TypeScript环境搭建
首先,确定Node版本,这里强调一下,本课程所有的项目均基于Node v14.17.0进行开发:
# 检查出版本没有14.17.0
$ nvm list
* 12.10.0 (Currently using 64-bit executable)
# 安装14.17.0并检查
$ nvm install 14.17.0
$ nvm use 14.17.0
$ nvm list
* 14.17.0 (Currently using 64-bit executable)
12.10.0
检查得到Node版本为14.17.0即可。
全局安装TS
$ npm i typescript -g
# 或者使用yarn
$ yarn add global typescript
安装完成后检查TS版本:
如果可以出来一个版本号,就代表已经全局安装成功。
1、创建项目与文件转换
创建好一个空白项目(如:ts-study)后,用vscode打开,创建一个名为 demo1.ts
的文件:
function fn(){
let str: string = "你好世界";
console.log(str);
}
fn();
然后打开终端,使用 node demo1.ts
运行该ts文件,会发现报错:
原因是ts需要转为js文件才能被识别。
因此,先使用 tsc demo1.ts
将文件进行转换,转换成功会看到当前目录下多了一个demo1.js,此时使用node去运行这份js文件,才能看到结果:
2、自动转换
如果每次都要先将ts文件转一次js文件,再运行js文件,那么过程太过繁琐,可以直接使用插件帮我们解决:
# 全局安装ts-node
$ npm i -g ts-node@8.5.4
安装成功后,删掉项目中的 demo1.js
,然后直接在终端运行:
如果可以直接得到运行结果,如下图,即代表运行成功。
三、简单数据类型
1、简单数据类型(掌握)
同JS一样,TS也有简单数据类型,而且大同小异。拿上述使用的代码:
function fn(){
let str: string = "你好世界";
console.log(str);
}
fn();
这里 str:string
中,str是变量名称,:string
是指定这个字段的类型为字符串,如果你给它赋值任何其他数据类型,都是不被允许的:
由此,我们了解了TS相较于JS,更加注重变量的数据类型校验。剩余的TS简单数据类型还包括:
数字number / bigint(一般较大的整数才会使用)
请注意:虽然number和bigint都表示数字,但是这两个类型不兼容。
2、静态类型检测(了解)
什么是静态类型呢?所谓静态类型,就是一个变量如果定义了它的类型,那么这个类型就不再允许修改。在编译时期,静态类型的编程语言即可准确地发现类型错误,这就是静态类型检测的优势。
来观看下面这段代码:
let str: string = "你好世界";
str = 123; // 错误的修改
在编译(转译)时期,TypeScript 编译器将通过对比检测变量接收值的类型与我们显示注解的类型,从而检测类型是否存在错误。如果两个类型完全一致,显示检测通过;如果两个类型不一致,它就会抛出一个编译期错误,告知我们编码错误,效果如下图所示。
这个静态类型检测在VsCode中已经完美集成,开发者只需要看提示即可了解需要修改的地方。
3、类型注释与推断(掌握)
当写完以下代码
function fn(one, two){
console.log(one + two);
}
let result = fn(1, 2);
然后鼠标移上形参one时,会得到一个提示。
当提示为any时,代表你需要给它写上类型:
function fn(one: number, two: number){
return one + two;
}
let result = fn(1, 2);
你加上的这两个number,就是类型注释。而此时,当你鼠标移上result时,就会得到一个新的提示:
这个提示告诉你result是个number,但我们并没有声明result是number呀!这就是类型推断。
四、复杂数据类型
同JS一样,TS也有复杂数据类型,包含以下几种:
特殊类型any、unknown、void、undefined、null、never、object
接下来将围绕这几种数据类型进行讲解。
1、数组类型Array(掌握)
一个数组的每一项其实是可以定义它的字段类型的:
let arr1: number[] = [1,2,3]
let arr2: string[] = ["1", "2", "3"]
如果没有按规定赋值,就会报错:
以上代码也可以用这种方式表示:
let arr1: Array<Number> = [1,2,3]
let arr2: Array<String> = ["1", "2", "3"]
注意:
建议优先使用第一种方式书写,否则在React的JSX语法中可能会冲突。下文中所有可以使用这类写法的,都不会讲。
当然,所有数组的操作也必须符合以上的数据类型定义:
arr2.push("4"); // 正确
arr2.push(4); // 错误
2、类型推断(掌握)
如果每个数组项都是不同的值,TS会自动给我们提示:
这就是类型推断。TS帮我们推断出这个数组共有多少种数据类型,因此你可以修改代码为:
let arr: (string | number | boolean)[] = ["张三", true, 16];
如此,开发者便可以较为智能的书写代码。
3、元组类型Tuple(了解)
如果数组中的每一项也是个数组,应该怎么写呢?这时候就需要依赖元组Tuple。
元组最重要的特性是可以限制数组元素的个数和类型,它特别适合用来实现多值返回。在 JavaScript 中并没有元组的概念,作为一门动态类型语言,它的优势是天然支持多类型元素数组。
let arr: [string, number][] = [
["张三", 13],
["李四", 14],
["王五", 15]
];
如果每个数组项不是数组,而是对象,那么写法如下:
let arr: {name: string, age: number}[] = [
{name: "刘备", age: 51},
{name: "关羽", age: 51},
{name: "张飞", age: 51}
]
4、任意类型any(掌握)
any 指的是一个任意类型,它是官方提供的绕过静态类型检测的方法。如:
let arr: any[] = [11, "你好世界"]
注意:
非必要不使用any,这几乎违背了TS的出发点。
5、不确定变量unknown(了解)
unknown 是 TypeScript 3.0 中添加的一个类型,它主要用来描述类型并不确定的变量。在 3.0 以前的版本中,只有使用 any 才能满足这种动态类型场景。与 any 不同的是,unknown 在类型上更安全。比如我们可以将任意类型的值赋值给 unknown,但 unknown 类型的值只能赋值给 unknown 或 any。
let str: unknown;
let num: string = str; // 不能将类型“unknown”分配给类型“string”。
let abc: any = str; // 没有报错
6、无返回值void(掌握)
当一个函数没有返回值时,需要加上void:
function fn(one: number, two: number): void{
console.log(123)
}
fn(1,2);
7、undefined与null(了解)
这两个没什么好讲的,此处省略。
8、never(了解)
never 表示永远不会发生值的类型。一个永远不会有返回值,或者产生死循环的函数,就可以加上never:
// 只有产生报错才会产生错误返回
function fn1(str: string): never {
throw Error(str);
}
// 只有条件为真才会产生值返回
function fn2(): never {
while(true) {}
}
9、object(了解)
object 类型表示非原始类型的类型,即非 number、string、boolean、bigint、symbol、null、undefined 的类型。
10、类型断言(了解)
如果一段js代码,你已经可以很明确的知道返回值一定是某种数据类型,就可以使用as给它断言:
const arr: number[] = [1,2,3,4]
const result: number = arr.find(num=>num>2) as number;
五、字面量类型(了解)
在 TypeScript 中,字面量不仅可以表示值,还可以表示类型,即所谓的字面量类型。
使用值所代表的数据类型来作为变量的数据类型,简单来说,即:值也可以作为类型使用。但要注意,目前只能是string、number和boolean三种数据类型可以写成字面量类型。
const str: "你好世界" = "你好世界";
const num: 123 = 123;
const bool: true = true;
六、返回值类型(掌握)
在上文讲述void的时候,其实已经使用过了,大致如下:
function fn(one: number, two: number): number{
return one+two;
}
fn(1,2);
七、剩余参数(了解)
在 ES6 中,JavaScript 支持函数参数的剩余参数,它可以把多个参数收集到一个变量中。同样,在TypeScript 中也支持这样的参数类型定义。
// 参数个数不确定,但要实现累加
function fn(num1: number, ...nums: number[]): number{
return nums.reduce((prev,next)=>prev+next, num1);
}
console.log( fn(1,2) );
console.log( fn(1,2,3) );
效果如图:
八、this(了解)
原生JS中的this指向非常模糊,函数中的this只有当函数被调用了才能知道this指向谁。而TS中,严格模式下必须显式地指定this指向,否则如下图所示:
因此代码应该改为:
function fn(this: Window, str: string){ // 这里Window是大写
console.log(this);
}
window.fn = fn; // 这里window是小写
window.fn("你好世界");
这里要注意:
1、这段代码不会成功执行,因为你是在ts-node环境下运行,不存在window,也不会有global;
2、这里的this是伪形参,不会被编译为js。
九、ES6面向对象
ES5的面向对象是由函数实现,加上TS的话按照上述内容即可。而ES6的面向对象是使用class来定义类,因此需要特殊讲一下:
1、类(掌握)
以下描述一个Animal类:
class Animal {
name: string; // 声明了Animal类中的name属性必须为string类型
constructor(name: string){
this.name = name;
}
sayName():void{ // 加void表示没有返回值
console.log(this.name);
}
}
let animal = new Animal("嘟嘟");
animal.sayName(); // 嘟嘟
2、继承(掌握)
定义一个Cat类,继承自Animal类:
class Animal {
name: string; // 声明了Animal类中的name属性必须为string类型
constructor(name: string){
this.name = name;
}
sayName():void{
console.log(this.name);
}
}
class Cat extends Animal {
constructor(name: string){
super(name);
}
shout(): void{
console.log(`${this.name}正在喊“喵喵”`);
}
}
let cat = new Cat("嘟嘟");
cat.shout(); // 嘟嘟正在喊“喵喵”
3、公共、私有与受保护的修饰符(了解)
类属性和方法除了可以通过 extends 被继承之外,还可以通过修饰符控制可访问性。
在 TypeScript 中就支持 3 种访问修饰符,分别是 public、private、protected。
a. 公共修饰符public
public 修饰的是在任何地方可见、公有的属性或方法。上述代码中,凡是没有加修饰符的,都是默认已经自带public:
class Animal {
name: string;
constructor(name: string){
this.name = name;
}
sayName():void{
console.log(this.name);
}
}
let animal = new Animal("张三");
animal.sayName();
// 以上代码相当于:
class Animal {
public name: string;
constructor(name: string){
this.name = name;
}
public sayName():void{
console.log(this.name);
}
}
let animal = new Animal("张三");
animal.sayName();
b. 私有修饰符private
private 修饰的是仅在当前类中可见、私有的属性或方法。具体如下:
class Animal {
public name: string;
private age: number; // 只能在当前类中使用,实例和子类都不能用
constructor(name: string, age: number){
this.name = name;
this.age = age;
}
public sayName():void{
console.log(`${this.name}今年${this.age}岁`);
}
}
let animal = new Animal("张三", 11);
animal.sayName(); // 张三今年11岁
console.log(animal.age); // 属性“age”为私有属性,只能在类“Animal”中访问 ✖
c. 受保护修饰符protected
protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法。
class Animal {
public name: string;
protected age: number; // 只能在当前类和子类中使用,实例不能用
constructor(name: string, age: number){
this.name = name;
this.age = age;
}
}
let animal = new Animal("张三", 11);
console.log(animal.age); // 属性“age”受保护,只能在类“Animal”及其子类中访问 ✖
class Cat extends Animal {
constructor(name: string, age: number){
super(name, age);
}
shout(): void{
console.log(`${this.name}正在喊“喵喵”,它今年${this.age}岁`); // 可以使用 ✔
}
}
let cat = new Cat("嘟嘟", 11);
cat.shout(); // 嘟嘟
4、只读属性与静态属性(了解)
a. 只读属性readonly
加上readonly的属性只能读不能写:
b. 静态属性static
加上static的属性与方法,无需实例化即可调用:
class Animal {
// 直接给静态属性赋值
static username: string = "张三";
}
Animal.username; // 张三
5、抽象类abstract(了解)
简单一句话:抽象类无法被实例化。
// 定义一个抽象类
abstract class Animal {
name: string;
constructor(name:string){
this.name = name;
}
}
let animal = new Animal("张三"); // 无法创建抽象类的实例。
十、接口类型
请先看这段js代码:
function fn({name, age}){
console.log(`${name}的年龄为:${age}`)
}
fn({name: "张三", age: 30}); // 张三的年龄为:30
它的作用是给函数传了个对象,并且把对象中两个字段解构出来使用。但如果你要指定两个字段的类型,就会报错了:
此时如果你通过以下代码书写,效果就不一样了:
function fn({name, age}: {name: string; age: number}){
console.log(`${name}的年龄为:${age}`)
}
fn({name: "张三", age: 30}); // 张三的年龄为:30
1、接口类型
其实这段代码是接口类型的演变,原来的表现形式应该如此:
// 使用接口定义字段类型
// 接口一般首字母大写,经常使用I开头
interface IObj {
name: string;
age: number;
}
function fn(obj: IObj): void{
console.log(`${obj.name}今年${obj.age}岁`)
}
fn({name: "张三", age: 33}); // 张三今年33岁
以上接口中,name和age的顺序是可以打乱的,不会影响代码执行。
2、可缺省属性
接口中定义的字段,参数必须传,但有些参数我们希望可传可不传,可以如下:
interface IObj {
name: string;
age?: number; // ?表示该参数可传可不传
}
function fn(obj: IObj): void{
console.log(`${obj.name}今年${obj.age || 0}岁`)
}
fn({name: "张三"}) // 张三今年0岁
3、任意属性
其实属性名称有时也不确定,因此可以使用以下方式定义,但值必须为any。
interface IObj {
name: string;
// [propName: string]: string | number;
// 这个参数名称可能也是不确定的,也不清楚它的类型,可以any,也可指定若干数据类型,建议直接any
[propName: string]: any;
}
function fn(obj: IObj): void{
console.log(`${obj.name}是${obj.sex || '男'}性`)
}
fn({name: "张三"}) // 张三是男性
fn({name: "张三", sex: "女"}) // 张三是女性
注意:
任意属性和可选属性最好不能共存,即便共存最好类型一致。
十一、泛型
泛型是指在定义函数、接口,或者类的时候,不预先指定具体的类型,而是在使用的时候在指定类型的一种特性。
现在有一个需求:有一个函数可以创建指定长度的数组,每一项都要填充一个指定值。
function createArr(length: number, value: any): Array<any>{
// 或:function createArr(length: number, value: any): any[]{
let arr = [];
for(var i=0;i<length;i++){
arr[i] = value;
}
return arr;
}
console.log( createArr(3, "猴子的救兵") ); // [ '猴子的救兵', '猴子的救兵', '猴子的救兵' ]
以上代码在函数调用前,是不清楚返回的数据类型的,因为value的数据类型为any。但我们说尽量不使用any,毕竟它绕过了类型检查,因此,我们需要借助泛型来解决。
1、泛型简单使用
一般我们用 T
来代表输入和返回的数据类型,直到调用了才明确 T
是什么数据类型:
function createArr<T>(length: number, value: T): Array<T>{
// 或:function createArr<T>(length: number, value: T): T[]{
let arr: T[] = [];
for(var i=0;i<length;i++){
arr[i] = value;
}
return arr;
}
console.log( createArr<string>(3, "猴子的救兵") ); // [ '猴子的救兵', '猴子的救兵', '猴子的救兵' ]
2、多个类型参数
如果有多个参数都是类型未知的,那么泛型应该这么写:
// 情况一:多个参数传入
function fn<U, T>(flag: U, value: T): Array<U | T>{
let arr: Array<U | T> = [flag, value];
return arr;
}
console.log(fn(true, "我是孙悟空")) // [ true, '我是孙悟空' ]
// 情况二:单个参数传入,且为数组
function fn<U, T>(arr: [U, T]): Array<U | T>{
let newArr: Array<U | T> = [...arr];
return newArr;
}
console.log(fn([true, "我是孙悟空"])) // [ true, '我是孙悟空' ]