36

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

 4 years ago
source link: https://blog.itlee.top/2019/11/06/TypeScript-从入门到放弃-一-:基础类型、接口、函数和类/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

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

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

基础类型

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

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

布尔值

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 表示没有任何返回值的函数。

function foo (): void {
    //...
}

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

Null 和 Undefined

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

let num: number = undefined
let str: string = undefined
//...

任意值及类型推断

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

let anyVar: any = 1
anyVar = '1'
anyVar = true

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

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

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

联合类型

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

let strOrNumber: string | number
strOrNumber = 1 
// 此时,只能访问 number 类型的方法。访问 length 会报错。

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

// strOrNumber = true // error

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

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

type StrOrNumber = string | number 
let strOrNum : StrOrNumber

数组

两种定义数组的方式:

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

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

元组

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

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

枚举类型

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

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

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

类型断言

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

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

复杂类型

接口

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

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

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

例如:

// 含有一个可选类型的接口。
interface Person {
  name: string;
  age: number;
  score?: number; // 可选属性
}
let per: Person = {
  name: 'owenlee',
  age: 27,
  // score: 99, // 可有可无
}

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

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。

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

interface Person {
  name: string;
  age: number;
  score?: number;
  [propName: string]: any;
}
let pTom: Person = {
  name: 'tom',
  age: 10,
  gender: 'm'
}
// [propName: string] 定义了任意属性取 string 类型的值。

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

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

interface numOrStr {
    [str: string]: string;
    [num: number]: string;
}

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

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

函数

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

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

// 函数声明
function sum (x, y) {
  return x + y
}
// 函数表达式
let summ = function (x, y) {
  return x + y
}

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

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
}

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

// 定义一个函数类型的接口
// 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 中类的定义很简单。

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 。只读属性必须在声明时或构造函数里被初始化。

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 定义,抽象类不允许被实例化。抽象类的抽象方法必须被子类实现。

// 抽象类定义方式
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()
})
// 输出:汪汪 喵喵

类和接口

类实现接口

interface Human {
  name: string;
  eat (): void;
}
// 用接口约束类的成员属性和方法。
class Asian implements Human {
  constructor(name: string) {
    this.name = name
  }
  name: string
  eat () { }
}
// 注意:类实现接口是必须实现接口所有的属性和方法。
// 只能约束共有成员或者方法
// 不可以约束构造函数

接口的继承

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

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 一个接口可以同时继承多个接口,就是将多个接口合并成一个接口。实现接口的时候需要把接口所有的属性和方法都实现。

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

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开发实践》中给出的接口和类关系图。

B73aYvn.png!web

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

小结

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

参考

  • TypeScript 文档
  • TypeScript 入门
  • 《极客时间 TypeScript 开发实践》

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK