Part1-TypeScript

一、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版本:

$ tsc --version

如果可以出来一个版本号,就代表已经全局安装成功。

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,然后直接在终端运行:

$ ts-node demo1.ts

如果可以直接得到运行结果,如下图,即代表运行成功。

三、简单数据类型

1、简单数据类型(掌握)

同JS一样,TS也有简单数据类型,而且大同小异。拿上述使用的代码:

function fn(){
    let str: string = "你好世界";
    console.log(str);
}

fn();

这里 str:string 中,str是变量名称,:string 是指定这个字段的类型为字符串,如果你给它赋值任何其他数据类型,都是不被允许的:

由此,我们了解了TS相较于JS,更加注重变量的数据类型校验。剩余的TS简单数据类型还包括:

  • 字符串string

  • 数字number / bigint(一般较大的整数才会使用)

  • 布尔值boolean

  • 唯一值symbol

请注意:虽然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也有复杂数据类型,包含以下几种:

  • 数组类型Array

  • 元组类型Tuple

  • 特殊类型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, '我是孙悟空' ]

最后更新于

这有帮助吗?