TypeScript学习笔记

  1. 1. 1.TypeScript介绍
    1. 1.1. TypeScript是什么
    2. 1.2. TypeScript 为什么要为 JS 添加类型支持?
    3. 1.3. TypeScript 相比 JS 的优势
  2. 2. 2.TypeScript初体验
    1. 2.1. 安装编译 TS 的工具包
    2. 2.2. 编译并运行 TS 代码
    3. 2.3. 简化运行 TS 的步骤
  3. 3. 3.TypeScript 常用类型
    1. 3.1. 概述
    2. 3.2. 类型注解
    3. 3.3. 原始类型
    4. 3.4. 数组类型和对象类型
    5. 3.5. 联合类型
    6. 3.6. 类型别名
    7. 3.7. 函数类型
      1. 3.7.1. 1 .单独指定参数、返回值的类型
      2. 3.7.2. 2 .同时指定参数、返回值的类型。
    8. 3.8. 可选参数
    9. 3.9. 对象类型
    10. 3.10. 接口
      1. 3.10.1. 接口与类型别名相比
      2. 3.10.2. 接口继承
    11. 3.11. 元组
    12. 3.12. 类型推论
    13. 3.13. 类型断言
    14. 3.14. 字面量类型
    15. 3.15. 枚举
    16. 3.16. any类型
    17. 3.17. typeof
  4. 4. 4.TypeScript高级类型
    1. 4.1. class类
      1. 4.1.1. 构造函数
      2. 4.1.2. 类的继承
      3. 4.1.3. 类成员可见性
      4. 4.1.4. 只读修饰符
    2. 4.2. 类型兼容性

1.TypeScript介绍

TypeScript是什么

TypeScript(简称:TS)是 JavaScript 的超集(JS 有的 TS 都有)。 TypeScript = Type + JavaScript(在 JS 基础之上,为 JS 添加了类型支持)。 TypeScript 是微软开发的开源编程语言,可以在任何运行 JavaScript 的地方运行。

1
2
3
4
5
// TypeScript代码:有明确的类型,即:number(数值类型)
let age1:numebr = 18

// JavaScript代码:无明确类型
let age2 = 18

TypeScript 为什么要为 JS 添加类型支持?

背景:JS 的类型系统存在“先天缺陷”,JS 代码中绝大部分错误都是类型错误(Uncaught TypeError)。

问题:增加了找 Bug、改 Bug 的时间,严重影响开发效率。 从编程语言的动静来区分,TypeScript 属于静态类型的编程语言,JS 属于动态类型的编程语言。

静态类型:编译期做类型检查;
动态类型:执行期做类型检查。
代码编译和代码执行的顺序:1 编译 2 执行

TypeScript 相比 JS 的优势

  1. 更早(写代码的同时)发现错误,减少找 Bug、改 Bug 时间,提升开发效率。
  2. 程序中任何位置的代码都有代码提示,随时随地的安全感,增强了开发体验。
  3. 强大的类型系统提升了代码的可维护性,使得重构代码更加容易。
  4. 支持最新的 ECMAScript 语法,优先体验最新的语法,让你走在前端技术的最前沿。
  5. TS 类型推断机制,不需要在代码中的每个地方都显示标注类型,让你在享受优势的同时,尽量降低了成本。

除此之外,Vue 3 源码使用 TS 重写、Angular 默认支持 TS、React 与 TS 完美配合,TypeScript 已成为大中型前端 项目的首先编程语言

2.TypeScript初体验

安装编译 TS 的工具包

问题:为什么要安装编译 TS 的工具包?
回答:Node.js和浏览器,只认识 JS 代码,不认识 TS 代码。需要先将 TS 代码转化为 JS 代码,然后才能运行。
安装命令:npm i -g typescript
typescript 包:用来编译 TS 代码的包,提供了 tsc 命令,实现了 TS -> JS 的转化。
验证是否安装成功:tsc –v(查看 typescript 的版本)。

编译并运行 TS 代码

  1. 创建 hello.ts 文件(注意:TS 文件的后缀名为 .ts)。
  2. 将 TS 编译为 JS:在终端中输入命令,tsc hello.ts(此时,在同级目录中会出现一个同名的 JS 文件)。
  3. 执行 JS 代码:在终端中输入命令,node hello.js

简化运行 TS 的步骤

问题描述:每次修改代码后,都要重复执行两个命令,才能运行 TS 代码,太繁琐。
简化方式:使用 ts-node 包,直接在 Node.js 中执行 TS 代码。
安装命令:npm i -g ts-node(ts-node 包提供了 ts-node 命令)。
使用方式:ts-node hello.ts
解释:ts-node 命令在内部偷偷的将 TS -> JS,然后,再运行 JS 代码。

3.TypeScript 常用类型

概述

TypeScript 是 JS 的超集,TS 提供了 JS 的所有功能,并且额外的增加了:类型系统。

  • l所有的 JS 代码都是 TS 代码。
  • JS 有类型(比如,number/string 等),但是 JS 不会检查变量的类型是否发生变化。而 TS 会检查。

TypeScript 类型系统的主要优势:可以显示标记出代码中的意外行为,从而降低了发生错误的可能性。 1. 类型注解 2. 常用基础类型

类型注解

示例代码:

1
2
3
4
5
6
let age:number = 18
// :number就是类型注解
// 作用:为变量添加类型约束;上述代码中,约定变量age的属性为number
// 约定了什么类型,就只能给变量赋值该类型的值,否则就会报错

let age:number = '18'// 不能将类型“String”分配给类型"number"

原始类型

原始类型:number/string/boolean/null/undefined/symbol

特点:完全按照js中类型的名称来书写

1
2
3
4
let age:numebr = 18
let myName:string = 'Qing'
let isLoading:boolean = false
// ...

数组类型和对象类型

对象类型:object(包括,数组、对象、函数等)
特点:对象类型在TS中更加细化,每个具体的对象都有自己的类型语法。

数组类型的两种写法:

1
2
let personList:string[] = ['a','b','c']
let numberList:Array<number> = [1,2,3]

联合类型

如果数组要求既有number,又有string,则可以使用联合类型

1
let arr:(number|string)[] = ['1','a',1,3]

可以由两个或多个其他类型组成的类型,表示可以是这些类型中的任意一种。

类型别名

为任意类型起别名(自定义类型)。当同一类型过于复杂,或被多次使用时,可以使用类型别名:

1
2
3
type CustomArray = Array<number|string>
let arr1:CustomArray = ['a',1]
let arr2:CustomArray = ['b',2]

函数类型

函数的类型实际上指的是:函数参数和返回值的类型。

为函数指定类型的两种方式:1 单独指定参数、返回值的类型 2 同时指定参数、返回值的类型。

1 .单独指定参数、返回值的类型

1
2
3
4
5
6
function add(num1:number,num2:number):number{
return num1+num2
}
function add2(num1:number,num2:number):number=>{
return num1+num2
}

2 .同时指定参数、返回值的类型。

1
2
3
cosnt add:(num1:number,num2:number) => numebr = (num1,num2) =>{
return num1 + num2
}

若没有返回值,可以设置函数返回类型为void

可选参数

使用函数实现某个功能时,参数可以传也可以不传。这种情况下,在给函数参数指定类型时,就用到可选参数了。

1
2
3
function mySelect(first?:number,second?:string):void{
console.log(first,second)
}

注意:可选参数只能出现在参数列表的最后,也就是说可选参数后面不能再出现必选参数。

对象类型

JS 中的对象是由属性和方法构成的,而 TS 中对象的类型就是在描述对象的结构(有什么类型的属性和方法)

对象类型写法:

1
2
3
4
5
let person:{name:string;age:number;sayHi():void,adress?: string} = {
name: 'qing',
age: 18,
sayHi:()=>{},
}

解释:

  1. 直接使用 {} 来描述对象结构。属性采用属性名: 类型的形式;方法采用方法名(): 返回值类型的形式。

  2. 如果方法有参数,就在方法名后面的小括号中指定参数类型(比如:greet(name: string): void)。

  3. 在一行代码中指定对象的多个属性类型时,使用 ;(分号)来分隔。

  • 如果一行代码只指定一个属性类型(通过换行来分隔多个属性类型),可以去掉 ;(分号)。
  • 方法的类型也可以使用箭头函数形式(比如:{ sayHi: () => void })
  • 对象同样可以使用可选参数,使用问好表示

接口

当一个对象类型被多次使用时,一般会使用接口(interface)来描述对象的类型,达到复用的目的。

解释:

  1. 使用 interface 关键字来声明接口。
  2. 接口名称(比如,此处的 IPerson),可以是任意合法的变量名称。
  3. 声明接口后,直接使用接口名称作为变量的类型。
  4. 因为每一行只有一个属性类型,因此,属性类型后没有 ;(分号)。
1
2
3
4
5
6
7
8
9
10
11
12
13
interface IPerson{
name: string,
age: number,
sayHi():void
}

let person:IPerson = {
name: 'hello world',
age: 18,
sayHi(){
console.log("hello")
}
}

接口与类型别名相比

相同点:都可以给对象指定类型
不同点:

  • 接口只能为对象指定类型
  • 类型别名,不仅可以为对象指定类型,实际上可以为任意类型指定别名

接口继承

如果两个接口之间有相同的属性或方法,可以将公共的属性或方法抽离出来,通过继承来实现复用。

比如,这两个接口都有 x、y 两个属性,重复写两次,可以,但很繁琐:

1
2
interface Point2D{x:number,y:number}
interface Point3D{x:numerb,y:number,z:numer}

使用继承:

1
2
interface Point2D{x:number,y:number}
interface Point3D extends Point2D{z:numer}

元组

场景:在地图中,使用经纬度坐标来标记位置信息。 可以使用数组来记录坐标,那么,该数组中只有两个元素,并且这两个元素都是数值类型

1
let position:number[] = [26.3215,69.3215]

使用 number[] 的缺点:不严谨,因为该类型的数组中可以出现任意多个数字。 更好的方式:元组(Tuple)。

元组类型是另一种类型的数组,它确切地知道包含多少个元素,以及特定索引对应的类型

1
let position:[number,number] = [26.3215,69.3215]

类型推论

TS 中,某些没有明确指出类型的地方,TS 的类型推论机制会帮助提供类型。 换句话说:由于类型推论的存在,这些地方,类型注解可以省略不写!

发生类型推论的 两 种常见场景:
1 . 声明变量并初始化时
2 . 决定函数返回值时

类型断言

有时候你会比 TS 更加明确一个值的类型,此时,可以使用类型断言来指定更具体的类型。 比如:

1
2
3
4
<a href="https://www.baidu.com" id="link">百度</a>
<script type="text/typescript">
const alink:HTMLElement = document.getElementById('link')
</script>

注意:getElementById 方法返回值的类型是 HTMLElement,该类型只包含所有标签公共的属性或方法,不包含 a 标签特有的 href 等属性。 因此,这个类型太宽泛(不具体),无法操作 href 等 a 标签特有的属性或方法。
解决方式:这种情况下就需要使用类型断言指定更加具体的类型

1
2
3
const alink = document.getElementById('link') as HTMLAnchorElement
// 或者可以写为
const alink = <HTMLAnchorElement>document.getElementById('link')

技巧:在浏览器控制台,通过 console.dir() 打印 DOM 元素,在属性列表的最后面,即可看到该元素的类型

字面量类型

打印以下两个代码的类型:

1
2
let str1 = 'hello ts' // string
const str2 = 'hello ts' // 'hello ts'

解释:

  1. str1 是一个变量(let),它的值可以是任意字符串,所以类型为:string。

  2. str2 是一个常量(const),它的值不能变化只能是 ‘Hello TS’,所以,它的类型为:’Hello TS’。

注意:此处的 ‘Hello TS’,就是一个字面量类型。也就是说某个特定的字符串也可以作为 TS 中的类型。 除字符串外,任意的 JS 字面量(比如,对象、数字等)都可以作为类型使用

使用模式:字面量类型配合联合类型一起使用。 使用场景:用来表示一组明确的可选值列表。 比如,在贪吃蛇游戏中,游戏的方向的可选值只能是上、下、左、右中的任意一个。

1
2
3
function changeDirection(direction:'up'|'down'|'left'|'right'){
console.log(direction)
}

上述代码中,参数 direction 的值只能是 up/down/left/right 中的任意一个,相比于 string 类型,使用字面量类型更加精确、严谨。

枚举

枚举的功能类似于字面量类型+联合类型组合的功能,也可以表示一组明确的可选值。

枚举:定义一组命名常量。它描述一个值,该值可以是这些命名常量中的一个

1
2
3
4
5
6
enum Direction{Up,Down,Left,Right}

function changeDirection(direction:Direction){
console.log(direction)
}
changeDirection(Direction.Up)

解释:

  1. 使用 enum 关键字定义枚举。
  2. 约定枚举名称、枚举中的值以大写字母开头。
  3. 枚举中的多个值之间通过 ,(逗号)分隔。
  4. 定义好枚举后,直接使用枚举名称作为类型注解

注意:形参 direction 的类型为枚举 Direction,那么,实参的值就应该是枚举 Direction 成员的任意一个。

问题:我们把枚举成员作为了函数的实参,它的值是什么呢?
通过将鼠标移入 Direction.Up,可以看到枚举成员 Up 的值为 0。枚举成员是有值的,默认为:从 0 开始自增的数值。我们把,枚举成员的值为数字的枚举,称为:数字枚举。

当然,也可以给枚举中的成员初始化值:

1
enum Direction{Up=10,Down,Left,Right}

字符串枚举:枚举成员的值是字符串。

1
enum Direction{Up='Up',Down='Down',Left='Left',Right='Right'}

注意:字符串枚举没有自增长行为,因此,字符串枚举的每个成员必须有初始值

枚举是 TS 为数不多的非 JavaScript 类型级扩展(不仅仅是类型)的特性之一。 因为:其他类型仅仅被当做类型,而枚举不仅用作类型,还提供值(枚举成员都是有值的)。 也就是说,其他的类型会在编译为 JS 代码时自动移除。但是,枚举类型会被编译为 JS 代码!

1
2
3
4
5
6
7
8
9
enum Direction{Up='Up',Down='Down',Left='Left',Right='Right'}
// 会被编译为以下js语言:
var Direction;
(funciton Direction{
Direction["Up"] = "Up";
Direction["Down"] = "Down";
Direction["Left"] = "Left";
Direction["Right"] = "Right";
})(Direction || (Direction = {}))

any类型

原则:不推荐使用 any!这会让 TypeScript 变为 “AnyScript”(失去 TS 类型保护的优势)。 因为当值的类型为 any 时,可以对该值进行任意操作,并且不会有代码提示。

1
2
3
4
5
6
let obj:any = {x:0}

obj.bar = 'hello'
obj()
const n:number = obj
// 以上操作都不会有任何类型错误提示,即使可能存在错误

隐式具有 any 类型的情况:1. 声明变量不提供类型也不提供默认值 2. 函数参数不加类型

typeof

TS 也提供了 typeof 操作符:可以在类型上下文中引用变量或属性的类型(类型查询)

使用场景:根据已有变量的值,获取该值的类型,来简化类型书写

1
2
3
4
let p = {x:1,y:2}
function formatPoint(point:{x:number,y:number}){}
// 可以直接写为
function formatPoint(point:typeof p){}

注意:

  1. 使用 typeof 操作符来获取变量 p 的类型,结果与第一种(对象字面量形式的类型)相同。
  2. typeof 出现在类型注解的位置(参数名称的冒号后面)所处的环境就在类型上下文(区别于 JS 代码)。
  3. typeof 只能用来查询变量或属性的类型,无法查询其他形式的类型(比如,函数调用的类型)

4.TypeScript高级类型

class类

TypeScript 全面支持 ES2015 中引入的 class 关键字,并为其添加了类型注解和其他语法(比如,可见性修饰符等) 。 class 基本使用,如下:

1
2
class Person{}
const p = new Person()

注意:

  1. 根据 TS 中的类型推论,可以知道 Person 类的实例对象 p 的类型是 Person。
  2. TS 中的 class,不仅提供了 class 的语法功能,也作为一种类型存在

实例属性初始化:

1
2
3
4
5
6
class Person{
age: number,
gender='男'
}
// 声明成员 age,类型为 number(没有初始值)。
// 声明成员 gender,并设置初始值,此时,可省略类型注解(TS 类型推论 为 string 类型)

构造函数

1
2
3
4
5
6
7
8
9
10
11
class Person{
age: number,
gender='男'

constructor(age:number,gender:string){
this.age = age
this.gender = gender
}
// 成员初始化(比如,age: number)后,才可以通过 this.age 来访问实例成员
// 需要为构造函数指定类型注解,否则会被隐式推断为 any;构造函数不需要返回值类型
}

类的继承

继承的两种方式:1 extends(继承父类) 2 implements(实现接口)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// extends
class Animal(){
move(){
console.log("moving")
}
}
class Dog extends Animal{
bark(){
console.log("wang wang~")
}
}
const dog = new Dog()
dog.move()
dog.bark()
// 通过 extends 关键字实现继承
// 子类 Dog 继承父类 Animal,则 Dog 的实例对象 dog 就同时具有了父类 Animal 和 子类 Dog 的所有属性和方法

// implements
interface Singable{
sing():void
}
class Person implements Singable{
sing(){
console.log("一人我饮酒醉,醉把佳人成双对~")
}
}
// 通过 implements 关键字让 class 实现接口
// Person 类实现接口 Singable 意味着,Person 类中必须提供 Singable 接口中指定的所有方法和属性。

类成员可见性

可以使用 TS 来控制 class 的方法或属性对于 class 外的代码是否可见。

可见性修饰符包括:1 public(公有的) 2 protected(受保护的) 3 private(私有的)。

  1. public:表示公有的、公开的,公有成员可以被任何地方访问,默认可见性
  2. protected:表示受保护的,仅对其声明所在类和子类中(非实例对象)可见。
  3. private:表示私有的,只在当前类中可见,对实例对象以及子类也是不可见的

只读修饰符

readonly:表示只读,用来防止在构造函数之外对属性进行赋值。

1
2
3
4
5
6
7
8
9
class Person{
readonly age:number = 18
constructor(age:number){
this.age = age
}
}
// 使用 readonly 关键字修饰该属性是只读的,注意只能修饰属性不能修饰方法
// 属性 age 后面的类型注解(比如,此处的 number)如果不加,则 age 的类型为 18 (字面量类型)。
// 接口或者 {} 表示的对象类型,也可以使用 readonly。

类型兼容性

两种类型系统:1. Structural Type System(结构化类型系统) 2. Nominal Type System(标明类型系统)

TS 采用的是结构化类型系统,也叫做 duck typing(鸭子类型),类型检查关注的是值所具有的形状.也就是说,在结构类型系统中,如果两个对象具有相同的形状,则认为它们属于同一类型。

1
2
3
4
5
6
7
class Point{x:number,y:number}
class Point2D{x:number,y:number}

const p:Point = new Point2D()
//Point 和 Point2D 是两个名称不同的类,变量 p 的类型被显示标注为 Point 类型,但是,它的值却是 Point2D 的实例,并且没有类型错误。
//因为 TS 是结构化类型系统,只检查 Point 和 Point2D 的结构是否相同(相同,都具有 x 和 y 两个属性,属性类型也相同)。
//但是,如果在 Nominal Type System 中(比如,C#、Java 等),它们是不同的类,类型无法兼容。

注意:在结构化类型系统中,如果两个对象具有相同的形状,则认为它们属于同一类型,这种说法并不准确。 更准确的说法:对于对象类型来说,y 的成员至少与 x 相同,则 x 兼容 y(成员多的可以赋值给少的)。

1
2
3
4
5
class Point{x:number,y:numebr}
class Point3D{x:number,y:numebr,z:number}
const p:Point = new Point3D()
// Point3D 的成员至少与 Point 相同,则 Point 兼容 Point3D。
// 所以,成员多的 Point3D 可以赋值给成员少的 Point。

除了 class 之外,TS 中的其他类型也存在相互兼容的情况,包括: 接口兼容性、函数兼容性等

接口之间的兼容性,类似于 class。并且,class 和 interface 之间也可以兼容:

1
2
3
4
5
6
7
interface Point {x: number; y: number }
interface Point2D {x: number; y: number }
let p1: Point
let p2: Point2D = p1
interface Point3D{x:number;y:number;z:number }
let p3: Point3D
p2 = p3

函数之间兼容性比较复杂,需要考虑:

  1. 参数个数 :参数多的兼容参数少的(或者说,参数少的可以赋值给多的)
  2. 参数类型 :相同位置的参数类型要相同(原始类型)或兼容(对象类型)
  3. 返回值类型:如果返回值类型是原始类型,此时两个类型要相同;如果返回值类型是对象类型,此时成员多的可以赋值给成员少的