TypeScript 从入门到放弃(一):基础类型、接口、函数和类

前言

声明:本文中大量的来自 《TypeScript 文档》。

TypeScriptJavaScript 的一个超集,主要提供了类型系统和对 ES6 的支持,它由 Microsoft 开发,代码开源于 GitHub 上。随着 TypeScript 的发展,很多的库都在使用它进行开发和重写,如Vue3.0就是通过TS进行开发的。如果不会 TypeScript 根本就读不懂源码。所以说目前来学习TypeScript正是时候 ,让我们一起入门 TypeScript吧!

PS: 为了方便书写下面行文 TypeScript 简写为 TSJavaScript 简写为 JS

更新:2020-7-31

类型系统

原始类型

众所周知 JS 分为原始数据类型和对象类型。原始数据类型:boolean, number, string, null, undefined 以及 Symbol。另外,TS 中还提供了 anynevertuplevoidenum 等。

在 TS 中使用了类型注解方式声明变量。如强类型语言中的变量类型一样,用于约束变量类型。

布尔值

1
2
3
4
5
6
7
8
let isDone: boolean = false
// 构造函数 Boolean 创建的对象不是布尔值。
// 错误:“boolean”是基元,但“Boolean”是包装器对象
// let booleanObj: boolean = new Boolean(1) // Error

let isError: boolean = Boolean(0)
// 直接使用 Boolean 返回 `boolean` 类型。

数值

使用 number 定义数值。需要注意的地方是ES6中的二进制和十进制表示法,都会被编译成十进制数值。

字符串

TS字符串与ES6中的字符串相同,同样支持模板字符串和插值。

特殊类型

空值

JS 中没有空值的概念,TS 中用 void 表示没有任何返回值的函数。

1
2
3
function foo (): void {
//...
}

空值变量取值只有:undefined 和 null。

Null 和 Undefined

NullUndefined 是基本数据类型,与 void 区别是,undefinednull 是所有类型的子类型。也就是说 undefined 可是赋值给任意类型变量。

1
2
3
let num: number = undefined
let str: string = undefined
//...

任意值(any)及类型推断

在 TS 中任意值类型,就是 JS 中定义的变量,可以接收任意类型的值。

1
2
3
4
5
6
7
8
9
let anyVar: any = 1
anyVar = '1'
anyVar = true

// 对于为指定类型的变量,默认是任意值类型。
let something
something = 1
something = true
something = 'aaa'

如果不指定具体类型,直接赋值的话 TS 根据值的类型进行推断。

1
2
let str = 'aaa' // 推断为 string 类型
// str = 1 // error,已经被推断为 string 类型,不可以在赋值 number 类型。

联合类型

联合类型表示定义的变量可以取多种类型中的一种。

1
2
3
4
5
6
7
8
let strOrNumber: string | number
strOrNumber = 1
// 此时,只能访问 number 类型的方法。访问 length 会报错。

strOrNumber = 'aaa'
// 访问 string 类型方法

// strOrNumber = true // error

联合类型可以根据值进行类型推断,从而访问不同类型的方法。

在 TS 中可以为类型起别名,使用 type 关键字。

1
2
type StrOrNumber = string | number 
let strOrNum : StrOrNumber

数组

两种定义数组的方式:

  • 在元素类型后加[],表示由此类型元素组成一个数组。
  • 使用数组泛型,Array<元素类型>
1
2
3
4
5
let list: number[] = [1, 2, 3]
let list1: Array<number> = [1, 2, 3]

// 联合类型数组
let list2: Array<number|string> = [1, 2, '3']

元组

元素类型是一个特殊的数组,已知元素数量和类型,并且元素类型可以不同。

1
2
3
let ta: [string, number]
ta = ['hello', 10]
// ta = [10, 'hello'] // error

枚举类型

枚举类型是对 JS 的一个补充,使用关键字 enum

1
2
enum Color { Red, Green, Blue }
let c: Color = Color.Red

默认情况下,枚举从 0 开始编号,也可以手动设置编号 Red = 1,后面的会自动加一。另外,可以通过编号查找名字 Color[2] 返回的是字符串。

类型断言

这是一种向编译器确认类型的操作。通常使用 as 关键字。

1
2
let someValue: any = 'aaaa'
let strLength: number = (someValue as string).length // 使用断言,告诉编译器 someValue 此时是字符串类型。

类型别名

TS 提供了一种类型注解设置别名的便捷语法,类型别名。

1
2
type StrOrNum = string | number 
let sample: StrOrNum // <--- 字符串 | 数值

与接口不同,可以为任意的类型注解提供类型别名。
接口 VS 别名 ?

  1. 类型注解通常用于为联合类型和交叉类型提供语义化的名称,或者为简单对象提供类型别名。
  2. 接口通常用于需要说明类型注解层次结构的地方,可以使用 implements 和 extends

复杂类型

接口

在 TS 中,使用接口来定义对象的类型。接口是一种可以描述行为的概念,具体操作由类去实现。

1
2
3
4
5
6
7
8
9
10
11
interface Person {
name: string;
age: number
}
let tom: Person = {
name: 'Tom',
age: 24,
// score: 100 // error score 并没有定义在 Person 接口中
}
// 接口中的 name 和 age 都必须实现。
// tom 不可以增加和减少属性个数。

上面的例子中,接口里的属性都是必须的。有些情况下,有些属性是不需要,那如何做?解决方法是使用 可选属性。只需在属性的后面添加一个 ? 表示此属性是可选属性。

例如:

1
2
3
4
5
6
7
8
9
10
11
// 含有一个可选类型的接口。
interface Person {
name: string;
age: number;
score?: number; // 可选属性
}
let per: Person = {
name: 'owenlee',
age: 27,
// score: 99, // 可有可无
}

一些对象的属性只在对象创建的时候修改值。可以使用 只读属性 ,属性名前加 readonly

1
2
3
4
5
6
interface Point {
readonly x: number;
readonly y: number;
}
let p1: Point = { x: 10, y: 20 }
// p1.x = 5 // error, x 是只读的。

readonly vs const
判断该用readonly还是const的方法是看要把它做为变量使用还是做为一个属性。 做为变量使用的话用 const,若做为属性则使用readonly。

有时候可能希望接口可以允许任意类型的属性,使用可索引类型属性。接口定义方式如下。

1
2
3
4
5
6
7
8
9
10
11
12
interface Person {
name: string;
age: number;
score?: number;
[propName: string]: any;
}
let pTom: Person = {
name: 'tom',
age: 10,
gender: 'm'
}
// [propName: string] 定义了任意属性取 string 类型的值。

注意:存在任意类型时,其他的类型必须是任意类型的子集。

可索引类型索引值可以是 字符串数字

1
2
3
4
interface numOrStr {
[str: string]: string;
[num: number]: string;
}

注意,数字索引类型的返回值是字符串索引类型返回值的子类。

以上只是接口基础使用,后面将会学习使用接口定义函数,以及定义类等。

函数

函数是 JS 程序的基础,在 TS 中同样重要,并且 TS 还为函数添加了额外的功能。下面来看看 TS 中如何定义函数吧。

首先回忆一下,在 JS 中两种函数定义方式:函数声明函数表达式

1
2
3
4
5
6
7
8
// 函数声明
function sum (x, y) {
return x + y
}
// 函数表达式
let summ = function (x, y) {
return x + y
}

在 TS 中对函数定义进行了约束。规定返回值类型,参数数量必须相等。

1
2
3
4
5
6
7
8
9
10
11
function sum (x: number, y: number): number {
return x + y
}
// 类型推断,summ 是 (x:number,y:number) => number类型的。
let summ = function (x: number, y: number): number {
return x + y
}
// 完整写法
let summm: (x: number, y: number) => number = function (x: number, y: number): number {
return x + y
}

TS 还提供了 可选参数,如下:

1
2
3
4
5
6
// c? 为可选参数,默认值为0
let sum = function(a: number, b: number, c?:number = 0):number {
return a + b + c
}
sum(1, 2) // 3
sum(1,2,3) // 6

通过接口和别名的方式定义函数。

1
2
3
4
5
6
7
8
// 定义一个函数类型的接口
// interface Sum {
// (x: number, y: number): number
// }
// 或者使用 type 声明一个类型别名。
type Sum = (x: number, y: number) => number
// 编写一个函数
let add: Sum = (x, y) => x + y

在函数中也可以还规定了 可选参数参数默认值剩余参数。可选参数必须在参数列表的末尾;默认参数不用在参数列表的末尾;剩余参数使用 ...rest方式获取函数中的剩余参数。

PS: 如果使用参数的默认值,需要传入 undefined 而不是 null。

在 ES6 中加入了 class,TS 中的类除了实现了 ES6 中的类的功能外,还添加了一些新的用法。例如,添加权限修饰符。

下面先复习一下,面向对象几个概念。来自《TypeScript入门

面向对象的三大特性:封装、继承、多态。

  • 封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据。
  • 继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性。
  • 多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如 Cat 和 Dog 都继承自 Animal,但是分别实现了自己的 eat 方法。此时针对某一个实例,我们无需了解它是 Cat 还是 Dog,就可以直接调用 eat 方法,程序会自动判断出来应该如何执行 eat。

类定义了数据的抽象特点,包含属性和方法。对象是类的实例,通过 new 生成。

  • 存取器(getter & setter):用以改变属性的读取和赋值行为。
  • 修饰符(Modifiers):修饰符是一些关键字,用于限定成员或类型的性质。比如 public 表示公有属性或方法。
  • 抽象类(Abstract Class):抽象类是供其他类继承的基类,抽象类不允许被实例化。抽象类中的抽象方法必须在子类中被实现。
  • 接口(Interfaces):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(implements)。一个类只能继承自另一个类,但是可以实现多个接口。

类的定义

如果有其他语言基础,TS 中类的定义很简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class TPerson {
name: string
constructor(name: string) {
this.name = name
}
say (): void {
console.log(this.name)
}
}
let per1 = new TPerson('owenlee')

// 继承
class TStudent extends TPerson {
score: number
constructor(name: string, score: number) {
super(name)
this.score = score
}
say (): void {
console.log(this.name, this.score)
}
}
let stu = new TStudent('owenlee', 100)

这里声明了一个 TPerson 类,有个 name 属性,一个构造函数和一个方法。在构造函数中对 name 进行赋值。方法中访问类 name 属性。最后一行使用 new 创建了一个 TPerson 实例。

TStudent 是继承自 TPerson,默认用于父类的属性和方法。同时也可以重写父类的方法。需要注意的一点是在子类的构造函数中必须调用 super(),它会执行基类的构造函数。 而且,在构造函数里访问 this的属性之前,一定要调用 super()。 这个是 TS 强制执行的一条重要规则。

如果,不想让子类使用父类的某些属性方法,该如何做呢?就需要权限修饰符登场。

权限修饰符

TS 提供了三种访问修饰符:publicprivateprotected

  • public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的。
  • private 修饰的属性或方法是私有的,不能被实例和子类访问。
  • protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的。

另外,可以将属性设置为只读的 readonly。只读属性必须在声明时或构造函数里被初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class TAnimal {
public name: string // 公开属性,默认为 public
private phone: string // 私有属性,只能自己访问,不可被子类访问。
protected age: string // 保护属性,只允许被继承
public constructor(name: string) {
this.name = name
}
}
// 继承
class Cat extends TAnimal {
readonly color: string // 只读属性
constructor(name: string) {
super(name)
console.log(this.name)
}
}

最后,类的静态成员使用 static 修饰,这种成员只能使用类名来使用。

抽象类和多态

抽象类使用关键字 abstract 定义,抽象类不允许被实例化。抽象类的抽象方法必须被子类实现。

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
29
30
31
32
33
34
35
36
37
38
39
// 抽象类定义方式
abstract class ABAnimal {
name: string;
constructor(name: string) {
this.name = name
}
abstract sayHi (): void; // 抽象方法
}
// 继承自抽象类
class ADog extends ABAnimal {
constructor(name: string) {
super(name)
}
eat () {
console.log(`${this.name} is eating`)
}
// 必须实现抽象类的抽象方法
sayHi () {
console.log('汪汪')
}
}
// 继承抽象方法
class ACat extends ABAnimal {
constructor(name: string) {
super(name)
}
sayHi () {
console.log('喵喵')
}
}
// 两个子类都实现了抽象方法 sayHi
let adog = new ADog('pipi')
let acat = new ACat('fwfw')
let animals: ABAnimal[] = [adog, acat]
// 动态调用子类中的方法的实现,这就是多态。
animals.forEach(item => {
item.sayHi()
})
// 输出:汪汪 喵喵

类和接口

类实现接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Human {
name: string;
eat (): void;
}
// 用接口约束类的成员属性和方法。
class Asian implements Human {
constructor(name: string) {
this.name = name
}
name: string
eat () { }
}
// 注意:类实现接口是必须实现接口所有的属性和方法。
// 只能约束共有成员或者方法
// 不可以约束构造函数

接口的继承

在 TS 中接口可以像类一样实现继承,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface Human {
name: string;
eat (): void;
}
// 接口的继承
interface Man extends Human {
run (): void
}
interface Child {
cry (): void
}
// 继承多个接口
interface Boy extends Man, Child { }
// 实现 Boy 接口
let boy: Boy = {
name: '',
eat () { },
run () { },
cry () { }
}

PS 一个接口可以同时继承多个接口,就是将多个接口合并成一个接口。实现接口的时候需要把接口所有的属性和方法都实现。

接口除了可以继承接口,还可以继承类。相当于把类的成员和方法都抽象了出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Auto {
state = 1
action () { }
}
// 接口继承自类
interface AutoInterface extends Auto {
// 抽离出类的成员和方法,包括私有和公有和保护的成员。
}

// 实现接口
class CAuto implements AutoInterface {
state: number = 2
action (): void {
console.log('CAuto action methods')
}
}
// 接口继承类的好处呢?
class Bus extends Auto implements AutoInterface {
// 此时不需要实现 state, action 方法,因为在父类已经实现的。
}

极客时间《TypeScript开发实践》中给出的接口和类关系图。

接口和类

接口和类都是可以继承的;类可以实现接口;接口也可以继承类的公有、私有和受保护的成员。

小结

本篇学习了 TS 中基础类型、函数、接口和类的基本用法,还有很多知识点没有涉及到后期的学习中会继续补充。

参考