TypeScript从入门到放弃(未完结)

概述

TypeScript 是 Microsoft 开发和维护的一种面向对象的编程语言。它是 JavaScript 类型的超集,它可以编译成纯 JavaScript。TypeScript 可以在任何浏览器、任何计算机和任何操作系统上运行,并且是开源的。

TypeScript 大致有如下几个特点:

  1. 静态输入:静态类型检查,可以在开发过程中检测潜在的错误;
  2. 可读性和易维护性:增加了静态类型、类、模块、接口和类型注解;
  3. 可用于开发大型的应用;
  4. 更好的协作:类型安全是一种在编码期间检测错误的功能,而不是在编译项目时检测错误。这为开发团队创建了一个更高效的编码和调试过程;

安装

在安装 typescript 之前,需要先安装 nodejs

然后再使用如下命令来安装 typescript 以及 ts-node:

// 全局安装
npm install -g typescript
npm install -g ts-node

// 局部安装
npm install -d typescript
npm install -d ts-node

安装完成后可以使用下列命令来查看版本号:

tsc -v

我们可以使用下列命令将 ts 文件编译成一个新的 js 文件:

tsc demo.ts

还可以直接使用下列命令执行 ts 文件:

ts-node demo.ts

传送门 - TypeScript初体验


数据类型

在 JS 中,数据类型分为基础类型和对象类型。

基础数据类型包括:boolean、number、string、null、undefined 以及 ES6 中的新增的类型 Symbol 和 BigInt。

下面我们就来了解一下 TS 的数据类型。

boolean

与 JS 一样,布尔类型的值只有 true、false。

let b1: boolean = true
let b2: boolean = false

number

在 TS 中,除了支持十进制和十六进制字面量,还支持 ES6 中引入的二进制和八进制字面量。

let n1: number = 1
let n2: number = 0xf00d
let n3: number = 0b1010
let n4: number = NaN
let n5: number = Infinity

console.log(n1, n2, n3, n4, n5);    // 1 61453 10 NaN Infinity

string

在 TS 中,支持使用 ES6 中的模板字符串。

let s1: string = 'hello'
let s2: string = s1 + ' ts'
let s3: string = `${s1} LqZww`

console.log(s2);    // hello ts
console.log(s3);    // hello LqZww

ECMAScript6入门 - 模板字符串


void

在 TS 中,用 void 表示没有任何返回值的函数。

function fn(): void {
  console.log("hello");
}

对一个变量声明 void 类型并没有什么用,因为它只能将变量赋值为 undefined 或 null。

let u: void = undefined
let n: void = null

let s: void = '1'    // Error
let b: void = true    // Error

null、undefined

在 TS 中,undefined 和 null 各自有自己的类型分别叫做 undefined 和 null。它们与 void 相似。

let u: undefined = undefined
let n: null = null

与 void 的区别是:undefined 和 null 是所有类型的子类型:

let s1: string = undefined
let s2: string = null

let b1: boolean = undefined
let b2: boolean = null

let n1: number = undefined
let n2: number = null

即所有类型都可以赋值为 undefined 和 null。但是如果启用了 strictNullChecks ,即启用严格的空检查,那么 null 只能赋值给自己,undefined 只能赋值给 void 和自己。

let n1: void = null    // Error
let n2: undefined = null    // Error
let n3: null = null

let u1: void = undefined
let u2: undefined = undefined
let u3: null = undefined    // Error

never

在 TS 中,never 类型表示的是那些永不存在的值的类型。

它可以用做类型注解:

let n: never

下面这两种情况可以是 never 类型:

// 1.
function fn1(): never {
  while (true) { }
}

// 2.
function fn2(): never {
  throw new Error('error')
}

即 never 类型可以赋值给从来不会有返回值的函数和总是会抛出错误的函数。

与 void 的区别:

  1. void 类型表示的是没有任何返回值的函数,never 类型表示的是一个函数从来不会有返回值或者总是抛出错误;
  2. void 类型可以将变量赋值 undefined、null,而 never 类型不行;

any

any 表示任意值,可以允许赋值为任意类型。

如果定义好了变量的类型,那么在后面的赋值过程中将不允许改变类型:

let s: string = '123'
s = 123    // Error

而如果是 any 类型,那么可以被改变为任意类型:

let a: any = 123
a = '123'
a = true

变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型:

let a1
a1 = 1
a1 = true

// 等价于

let a2: any
a2 = 1
a2 = true

array

在 TS 中,数组类型有多种定义方式:

  1. 类型 + 方括号:number[]、string[]
  2. 数组泛型(Array ):Array、Array
let arr1: number[] = [1, 2, 3]
let arr2: Array<number> = [1, 2, 3]

let arr3: (string | number)[] = [1, 2, '3']
let arr4: Array<number | string> = [1, 2, '3']

let arr5: any = [1, '2', true]

元组

数组合并了相同类型的对象,而元组(Tuple)合并了不同类型的对象。

let tuple: [string, number, boolean] = ['1', 2, false]
tuple[0] = '2'

console.log(tuple[0]);    // 2
console.log(tuple[2]);    // false

枚举

枚举使用 enum 关键字来定义。默认情况下,从 0 开始为元素编号。

enum Colors {
  Red, Blue, White, Yellow
}

console.log(Colors[0] === 'Red');    // true
console.log(Colors['Red'] === 0);    // true

我们可以给枚举项手动进行赋值:

enum Colors {
  Red, Blue = 5, White, Yellow
}

console.log(Colors[5]);    // Blue
console.log(Colors['Red']);    // 0
console.log(Colors['White']);    // 6

未手动赋值的枚举项会接着上一个枚举项递增。


object

object 表示非原始类型,也就是除 number、string、boolean、symbol、null或undefined之外的类型。

使用 object 类型,就可以更好的表示像 Object.create 这样的API。例如:

declare function create(o: object | null): void;

create({ prop: 0 });    // OK
create(null);    // OK

create(42);    // Error
create("string");    // Error
create(false);    // Error
create(undefined);    // Error

类型断言

类型断言有两种形式:

  1. 尖括号语法
  2. as 语法
let s: any = "this is a string";

let l1: number = (<string>s).length;

let l2: number = (s as string).length;

接口

接口使用 interface 来表示。接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。

基本使用

下面来简单使用下接口:

interface Info {
  name: string,
  age: number
}
function getInfo({ name, age }: Info) {
  return `${name},${age}`
}
console.log(getInfo({
  name: 'LqZww',
  age: 19
}));

可选属性

接口还提供了可选属性,在可选属性名字定义的后面加一个 ? 符号:

interface Info {
  name: string,
  age?: number
}
function getInfo({ name, age }: Info) {
  return `${name},${age ? age : ''}`
}
console.log(getInfo({
  name: 'LqZww'
}));

绕开额外属性检查

当我们在使用函数时,额外的传入了其他未定义的属性(这个多传入的属性并不会影响我们程序的执行结果),编译器将会提示报错,那我们如何绕开这些多余属性的检查呢?

有以下三种方式可以绕开额外的属性检查:

  1. 类型断言
  2. 索引签名
  3. 类型兼容性
// 1. 类型断言
console.log(getInfo({
  name: 'LqZww',
  age: 19,
  size: 11
} as Info));

// 2. 索引签名
interface Info {
  name: string,
  age?: number,
  [propName: string]: any
}

// 3. 类型兼容性
let myInfo = {
  name: 'LqZww',
  age: 19,
  size: 11
}
console.log(getInfo(myInfo));

只读属性

一些对象属性只能在对象刚刚创建的时候修改其值。可以在属性名前用 readonly 来指定只读属性:

let infoObj: Info = {
  name: 'LqZww'
}
infoObj.name = 'hhh'    // 报错

我们还可以定义个数组接口,第一项为只读属性:

interface ArrInfo {
  readonly 0: number,
  1: string,
  2: boolean
}
let arr: ArrInfo = [1, '2', true]

arr[0] = 2    // 报错

函数类型

接口能够描述 JavaScript 中对象拥有的各种各样的外形。除了描述带有属性的普通对象外,接口也可以描述函数类型。

为了使用接口表示函数类型,我们需要给接口定义一个调用签名。 它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。

interface AddInfoFun {
  (num1: number, num2: number): number
}

let add: AddInfoFun = (n1, n2) => n1 + n2
console.log(add(1, 2));    // 3

可索引的类型

可索引类型具有一个索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。

interface StringArray {
  [id: number]: string
}
let sa: StringArray = {
  0: 'a',
  1: 'b'
}
console.log(sa);    // { '0': 'a', '1': 'b' }
console.log(sa[0]);    // a

接口继承

和类一样,接口也可以相互继承。这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。

interface People {
  sex: string
}
interface Man extends People {
  boy: string
}
let man: Man = {
  sex: '男',
  boy: 'boy'
}

一个接口可以继承多个接口,创建出多个接口的合成接口:

interface People {
  sex: string
}
interface Age {
  age: number
}
interface Man extends People, Age {
  boy: string
}
let man: Man = {
  sex: '男',
  boy: 'boy',
  age: 18
}

混合类型

接口能够描述 JavaScript 里丰富的类型。 因为 JavaScript 其动态灵活的特点,有时你会希望一个对象可以同时具有上面提到的多种类型。

interface Counter {
  (): void,
  count: number
}
let getCount = (): Counter => {
  const co = () => {
    co.count++
  }
  co.count = 0
  return co
}
let counter: Counter = getCount()
counter()
console.log(counter.count);

类型别名

类型别名用来给一个类型起个新名字,使用 type 创建类型别名。

例如下列代码:

type isString = string
type isNumber = number
function getInfo(x: isNumber, y: isNumber) {
  return x + y
}

type GetArray = <T>(arg: T, times: number) => T[]
let getArray: GetArray = (arg: any, times: number) => {
  return new Array(times).fill(arg)
}
console.log(getArray(1, 3));

type fn = () => string
type more = string | number | boolean

函数

函数类型

我们可以给每个参数添加类型之后再为函数本身添加返回值类型。 TypeScript 能够根据返回语句自动推断出返回值类型,因此我们通常省略它。

function add1(x: number, y: number): number {
  return x + y
}

// 箭头函数
let add2 = (x: number, y: number) => { return x + y }

我们已经为函数指定了类型,下面我们可以写出函数的完整类型:

let add: (x: number, y: number) => number
add = (x: number, y: number) => x + y

可选参数

JavaScript 里,每个参数都是可选的,可传可不传。没传参的时候,它的值就是 undefined。在 TypeScript 里我们可以在参数名旁使用 ? 实现可选参数的功能。

注意:可选参数必须跟在必须参数后面!

type AddFun = (arg1: number, arg2: number, arg3?: number) => number
let addFun1: AddFun = (x: number, y: number) => x + y
let addFun2: AddFun = (x: number, y: number, z: number) => x + y + z

默认参数

在 TypeScript 里,我们也可以为参数提供一个默认值当用户没有传递这个参数或传递的值是 undefined 时,它们叫做有默认初始化值的参数。

let addFun = (x: number, y: number = 10) => x + y

console.log(addFun(1));    // 11
console.log(addFun(1, 2));    // 3

注意:如果带默认值的参数出现在必须参数前面,必须明确的传入 undefined 值来获得默认值。


剩余参数

剩余参数会被当做个数不限的可选参数。可以一个都没有,同样也可以有任意个。编译器创建参数数组,名字是你在省略号(...)后面给定的名字,你可以在函数体内使用这个数组。

let add = (arg1: number, arg2: number, ...args: number[]) => {
  console.log(args);    // [ 4, 5, 6 ]
}
add(1, 2, 4, 5, 6)

重载

重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。

function getData(x: string): string[]
function getData(x: number): number[]
function getData(x: any): any {
  if (typeof x === 'string') {
    return x.split('')
  } else {
    return x * 2
  }
}
console.log(getData(1));    // 2
console.log(getData('1'));    // [ '1' ]

泛型

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

基本使用

function identity<T>(x: T): T {
  return x
}
console.log(identity<number>(2));
console.log(identity<number>('2'));    // 报错
console.log(identity<string>('1'));

在定义泛型的时候,还可以一次定义多个类型参数:

const getArray = <T, U>(p1: T, p2: U, times: number): Array<[T, U]> => {
  return new Array(times).fill([p1, p2])
}
getArray<number, string>(1, '2', 3).forEach((item) => {
  console.log(item);    // [ 1, '2' ] [ 1, '2' ] [ 1, '2' ]
})

泛型约束

首先看下例代码:

let getArray = <T>(arg: T, times: number): T[] => {
  return new Array(times).fill(arg)
}
getArray('1', 2)
getArray(1, 2)

当我们想要访问 arg 的 length 属性时,由于 number 并没有 length 属性,我们想要传入 number 时就会报错,但是上面代码并不会出现报错。因此就需要使用到泛型约束,给泛型变量做一个限制,创建一个 length 属性的接口,并使用 extends 关键字来实现约束,代码如下:

interface Lengthwise {
  length: number
}
let getArray = <T extends Lengthwise>(arg: T, times: number): T[] => {
  return new Array(times).fill(arg)
}
getArray('1', 2)
getArray(1, 2)    // 报错,类型“number”的参数不能赋给类型“Lengthwise”的参数。

下面我们来看看在泛型约束中使用类型参数:

我们可以声明一个类型参数,且它被另一个类型参数所约束。

比如,现在我们想要用属性名从对象里获取这个属性。 并且我们想要确保这个属性存在于对象 obj 上,因此我们需要在这两个类型之间使用约束。

let getProperty = <T, K extends keyof T>(obj: T, key: K) => {
  return obj[key]
}
let obj = {
  a: 'a',
  b: 'b'
}
getProperty(obj, 'a')
getProperty(obj, 'c')    // 报错,类型“"c"”的参数不能赋给类型“"a" | "b"”的参数。

基本使用

不了解 class 使用方法的可以先跳转至 ECMAScript 6 中的 class 语法了解下。

我们先来简单的使用下 TS 中的类的使用:

class Point {
  x: number
  y: number
  constructor(x: number, y: number) {
    this.x = x
    this.y = y
  }
  getPosition() {
    return `(${this.x},${this.y})`
  }
}
let p = new Point(1, 2)
console.log(p);    // Point { x: 1, y: 2 }

继承

在 TS 中,我们一样可以使用 class 中的 extends 来实现继承。

class Parent {
  name: string
  age: number
  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
}
class Son extends Parent {
  constructor(name: string, age: number) {
    super(name, age)
  }
}
let s = new Son('zww', 18)
console.log(s);    // Son { name: 'zww', age: 18 }

修饰符

public

public 表示公共的(默认值),用来指定在创建实例后可以通过实例访问的,也就是类定义的、外部的可以访问的属性和方法。

class Point {
  public x: number
  public y: number
  constructor(x: number, y: number) {
    this.x = x
    this.y = y
  }
  public getPosition() {
    return `(${this.x},${this.y})`
  }
}

private

private 表示私有的,它就不能在声明它的类的外部访问。

class Parent {
  public name: string
  private age: number
  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
}
let p = new Parent('zww', 18)
console.log(p.name);    // zww
console.log(p.age);    // 报错,属性“age”为私有属性,只能在类“Parent”中访问。
console.log(Parent.age);    // 报错,属性“age”为私有属性,只能在类“Parent”中访问。

class Son extends Parent {
  constructor(name: string, age: number) {
    super(name, age)
    console.log(super.age);    // 报错,属性“age”为私有属性,只能在类“Parent”中访问。
  }
}

protected

protected 表示受保护的,protected 修饰符与 private 修饰符的行为很相似,但有一点不同,protected 成员在派生类中仍然可以访问。

class Parent {
  protected age: number
  constructor(age: number) {
    this.age = age
  }
  protected getAge() {
    return this.age
  }
}
class Son extends Parent {
  constructor(age: number) {
    super(age)
    console.log(super.age);    // undefined
    console.log(super.getAge());    // 18
  }
}
let s = new Son(18)

readonly修饰符

可以使用 readonly 关键字将属性设置为只读的。只读属性必须在声明时或构造函数里被初始化。

class Info {
  readonly name: string
  constructor(name: string) {
    this.name = name
  }
}
let i = new Info('zww')
console.log(i.name);    // zww
i.name = 'lq'    // 报错,无法分配到 "name" ,因为它是只读属性。

参数属性

参数属性可以方便地让我们在一个地方定义并初始化一个成员。

下面是参数属性的使用:

class Info {
  constructor(public name: string) {

  }
}
let i = new Info('zww')
console.log(i);    // Info { name: 'zww' }

参数属性通过给构造函数参数前面添加一个访问限定符来声明。 使用 private 限定一个参数属性会声明并初始化一个私有成员;对于 public 和 protected 来说也是一样。


静态属性

class Info {
  public static myname: string = 'zww'
  public static getName() {
    return Info.myname
  }
  constructor() { }
}
let i = new Info()
console.log(i.myname);    // 报错,属性 "myname" 不是类型为 "Info" 的静态成员。
console.log(Info.myname);    // zww

可选类属性

class Info {
  public name: string
  public age?: number
  constructor(name: string, age?: number) {
    this.name = name
    this.age = age
  }
}
let i1 = new Info('zww')
let i2 = new Info('zww', 18)
console.log(i1);    // Info { name: 'zww', age: undefined }
console.log(i2);    // Info { name: 'zww', age: 18 }

存取器

TypeScript 支持通过 getters / setters 来截取对对象成员的访问。

class Info {
  public name: string
  public age?: number
  private _infoStr!: string
  constructor(name: string, age?: number) {
    this.name = name
    this.age = age
  }
  get infoStr(): string {
    return this._infoStr
  }
  set infoStr(value: string) {
    console.log(`setter:${value}`);
    this._infoStr = value
  }
}
let i = new Info('zww', 18)
i.infoStr = 'hhhh'    // setter:hhhh
console.log(i.infoStr)    // hhhh

抽象类

抽象类做为其它派生类的基类使用。它们一般不会直接被实例化。不同于接口,抽象类可以包含成员的实现细节。 abstract 关键字是用于定义抽象类和在抽象类内部定义抽象方法。

abstract class People {
  constructor(public name: string) { }
  public abstract printName(): void
}
class Man extends People {
  constructor(name: string) {
    super(name)
    this.name = name
  }
  public printName() {
    console.log(this.name);
  }
}
let m = new Man('zww')
m.printName()    // zww

枚举

使用枚举我们可以定义一些带名字的常量。 使用枚举可以清晰地表达意图或创建一组有区别的用例。 TypeScript 支持数字的和基于字符串的枚举。

数字枚举

enum Direction {
  Up,
  Down,
  Left,
  Right
}
console.log(Direction.Up);    // 0
console.log(Direction['Down']);    // 1

我们可以给枚举项手动进行赋值:

enum Direction {
  Up,
  Down = 2,
  Left,
  Right = 7
}
console.log(Direction.Up);    // 0
console.log(Direction.Down);    // 2
console.log(Direction.Left);    // 3
console.log(Direction.Right);    // 7

字符串枚举

字符串枚举的概念很简单,但是有细微的 运行时的差别。 在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。

enum Message {
  Success = 'success',
  Error = 'error',
  Failed = Error
}
console.log(Message.Success);    // success
console.log(Message.Failed);    // error

异构枚举

也就是混合字符串和数字成员。

enum Info {
  Name = 'zww',
  Age = 18
}
console.log(Info.Name);    // zww
console.log(Info.Age);    // 18

不建议这样做!


联合枚举与枚举成员的类型

存在一种特殊的非计算的常量枚举成员的子集:字面量枚举成员。字面量枚举成员是指不带有初始值的常量枚举成员,或者是值被初始化为:

  • 任何字符串字面量(例如: "foo", "bar", "baz")
  • 任何数字字面量(例如: 1, 100)
  • 应用了一元 - 符号的数字字面量(例如: -1, -100)
enum Animals {
  Dog = 1,
  Cat = 2
}
interface Dog {
  type: Animals.Dog
}
let dog: Dog = {
  type: Animals.Dog
}

类型推论

类型推论,即类型是在哪里如何被推断的。

let age = 18
age = '18'    // 报错,当我们定义 age 并赋值时,类型被推断为数字,当我们修改为其他类型将会报错。
let arr = [1, '2']
arr = [3, '4', false]    // 报错,当我们定义 arr 时,类型被推断为 (string | number)[] ,当我们赋值其他类型时将会报错。
// 报错
window.onmousedown = function (mouseEvent) {
  console.log(mouseEvent.button);
};

这个例子会得到一个类型错误,TypeScript 类型检查器使用 Window.onmousedown 函数的类型来推断右边函数表达式的类型。因此,就能推断出 mouseEvent 参数的类型了。如果函数表达式不是在上下文类型的位置,mouseEvent 参数的类型需要指定为 any,这样也不会报错了。


类型兼容性

基础

TypeScript 结构化类型系统的基本规则是,如果 x 要兼容 y,那么 y 至少具有与 x 相同的属性。

interface Named {
  name: string,
  info: {
    age: number
  }
}
let x: Named
let y1 = { name: 'lq' }
let y2 = { name: 'zww', info: { age: 18 } }
let y3 = { name: 'zww', info: { age: 18 }, like: 'game' }

x = y1    // 报错,类型 "{ name: string; }" 中缺少属性 "info",但类型 "Named" 中需要该属性。
x = y2
x = y3

注意,y3 有个额外的 like 属性,但这不会引发错误。只有目标类型(这里是Named)的成员会被一一检查是否兼容。

这个比较过程是递归进行的,检查每个成员及子成员。


函数

参数个数

let x = (a: number) => 0
let y = (b: number, c: string) => 0
y = x
x = y    // 报错

要查看 x 是否能赋值给 y,首先看它们的参数列表。x 的每个参数必须能在 y 里找到对应类型的参数。注意的是参数的名字相同与否无所谓,只看它们的类型。这里,x 的每个参数在 y 中都能找到对应的参数,所以允许赋值。而第二个赋值错误,是因为 y 有个必需的第二个参数,但是 x 并没有,所以不允许赋值。也就是说,右边的参数个数要小于左边的参数个数。

参数类型

let x = (a: number) => 0
let y = (b: string) => 0
x = y    // 报错
y = x    // 报错

参数的个数相同,但是 x、y 的类型不同,不管是 x 赋值给 y,还是 y 赋值给 x,都将会报错。

可选参数及剩余参数

let getSum = (arr: number[], callback: (...args: number[]) => number): number => {
  return callback(...arr)
}
let res1 = getSum([1, 2, 3], (...args: number[]): number => args.reduce((a, b) => a + b, 0))
let res2 = getSum([1, 2, 3], (arg1: number, arg2: number, arg3: number): number => arg1 + arg2 + arg3)
console.log(res1);    // 6
console.log(res2);    // 6

重载

对于有重载的函数,源函数的每个重载都要在目标函数上找到对应的函数签名。这确保了目标函数可以在所有源函数可调用的地方调用。

function merge(arg1: number, arg2: number): number
function merge(arg1: string, arg2: string): string
function merge(arg1: any, arg2: any) {
  return arg1 + arg2
}
console.log(merge(1, 2));
console.log(merge('1', '2'));

枚举

枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的。

enum Around { Right, Left }
enum Fluctuate { Up, Down }
let a = Around.Right
a = 10

a = Fluctuate.Up    // 报错,不同枚举类型之间不兼容。

类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。比较两个类类型的对象时,只有实例的成员会被比较。静态成员和构造函数不在比较的范围内。

class Father {
  public static age: number
  constructor(public name: string) { }
}
class Mother {
  public static age: string
  constructor(public name: string) { }
}
class Son {
  constructor(public name: number) { }
}
let father: Father
let mother: Mother
let son: Son

father = mother
father = son    // 报错

类的私有成员和受保护成员会影响兼容性。当检查类实例的兼容时,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。同样地,这条规则也适用于包含受保护成员实例的类型检查。这允许子类赋值给父类,但是不能赋值给其它有同样类型的类。


泛型

由于 TypeScript 是结构性的类型系统,类型参数只影响使用其做为类型一部分的结果类型。

interface Data<T> { }
let data1: Data<number>
let data2: Data<number>
data1 = data2
data2 = data1

高级类型

交叉类型

交叉类型是将多个类型合并为一个类型。我们可以把多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。用 & 符号来定义。

let mergeFun = <T, U>(arg1: T, arg2: U): T & U => {
  let res = <T & U>{}
  res = Object.assign(arg1, arg2)
  return res
}
console.log(mergeFun({ a: 'a' }, { b: 'b' }));    // { a: 'a', b: 'b' }

联合类型

联合类型表示一个值可以是几种类型之一。一般用竖线( | )分隔每个类型。

如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。

let getLength = (content: string | number): number => {
  if (typeof content === 'string') {
    return content.length
  } else {
    return content.toString().length
  }
}
console.log(getLength('abc'));    // 3
console.log(getLength(12345));    // 5

类型保护

我们先来看看下面这个例子:

let valueList = [123, 'abc']
let getValue = () => {
  let number = Math.random() * 10
  if (number < 5) {
    return valueList[0]
  } else {
    return valueList[1]
  }
}
let item = getValue()
if (item.length) {
  console.log(item.length);
} else {
  console.log(item.toFixed());
}

上面代码中,最后一个 if 代码段将会报错,因为 item 为联合类型,即 string | number。此时,我们可以使用断言来解决这个问题。

if ((item as string).length) {
  console.log((item as string).length);
} else {
  console.log((item as number).toFixed());
}

使用断言这个方法就有一个缺点,只要使用到了 item,我们就要给它断言,这样加大了代码的复杂性。因此,我们可以使用类型保护再来解决此问题。

typeof类型保护

function isString(value: number | string): value is string {
  return typeof value === 'string'
}
if (isString(item)) {
  console.log(item.length);
} else {
  console.log(item.toFixed());
}

typeof 类型保护只有两种形式能被识别:

  1. typeof v === "typename"
  2. typeof v !== "typename"
    并且 typename 必须是 number、string、boolean 或 symbol

instanceof类型保护

instanceof 类型保护是通过构造函数来细化类型的一种方式。

class Demo1 {
  public name = 'zww'
}
class Demo2 {
  public age = 18
}
function getDemo() {
  return Math.random() > 0.5 ? new Demo1() : new Demo2()
}
let item = getDemo()
if (item instanceof Demo1) {
  console.log(item.name);
} else {
  console.log(item.age);
}

instanceof 的右侧要求是一个构造函数,TS 将细化为:

  1. 此构造函数的 prototype 属性的类型,如果它的类型不为 any 的话
  2. 构造签名所返回的类型的联合

字符串字面量类型

字符串字面量类型允许你指定字符串必须的固定值。

type Name = 'zww'
let myName: Name = 'zww'
let youName: Name = 'lq'    // 报错

字符串字面量类型还可以与联合类型使用:

type Game = 'tlbb' | 'yxlm' | 'wzry'
function getGame(game: Game) {
  return game.substr(0, 1)
}
console.log(getGame('tlbb'));
console.log(getGame('hhhh'));    // 报错

数字字面量类型

type Age = 18
interface InfoInterface {
  name: string,
  age: Age
}
let info1: InfoInterface = {
  name: 'zww',
  age: 18
}
let info2: InfoInterface = { name: 'lq', age: 222 }    // 报错

可辨识联合

它具有 3 个要素:

  1. 具有普通的单例类型属性 — 可辨识的特征;
  2. 一个类型别名包含了那些类型的联合 — 联合;
  3. 此属性上的类型保护;
interface Square {
  kind: "square";
  size: number;
}
interface Rectangle {
  kind: "rectangle";
  width: number;
  height: number;
}
interface Circle {
  kind: "circle";
  radius: number;
}
type Shape = Square | Rectangle | Circle
function getArea(s: Shape): number {
  switch (s.kind) {
    case "square": return s.size * s.size;
    case "rectangle": return s.height * s.width;
    case "circle": return Math.PI * s.radius ** 2;
  }
}

完整性检查
当没有涵盖所有可辨识联合的变化时,我们想让编译器可以通知我们。有两种方式进行完整性检查。

  1. 启用 strictNullChecks
function getArea(s: Shape): number {    // 报错,函数缺少结束 return 语句,返回类型不包括 "undefined"。
  switch (s.kind) {
    case "square": return s.size * s.size;
    case "rectangle": return s.height * s.width;
  }
}

因为 switch 没有包涵所有情况,所以 TypeScript 认为这个函数有时候会返回 undefined。如果你明确地指定了返回值类型为 number,那么你会看到一个错误,因为实际上返回值的类型为 number | undefined。 然而,这种方法存在些微妙之处且 --strictNullChecks对旧代码支持不好。

  1. 使用 never 类型
function assertNever(value: never): never {
  throw new Error('Unexpected object: ' + value)
}
function getArea(s: Shape): number {
  switch (s.kind) {
    case "square": return s.size * s.size;
    case "rectangle": return s.height * s.width;
    // case "circle": return Math.PI * s.radius ** 2;
    default: return assertNever(s)    // 报错,类型“Circle”的参数不能赋给类型“never”的参数。
  }
}

这里, assertNever 检查 s 是否为 never 类型,即为除去所有可能情况后剩下的类型。如果你忘记了某个 case,那么 s 将具有一个真实的类型并且你会得到一个错误。


多态的this类型


参考资料

TypeScript 官网
TypeScript 中文网
TypeScript 入门教程
TypeScript 中文手册

posted @ 2020-11-23 22:57  LqZww  阅读(273)  评论(0编辑  收藏  举报