TypeScript 学习笔记

TypeScript 教程 - 网道icon-default.png?t=N7T8https://wangdoc.com/typescript/

1、简介

  • 概述
    • TypeScript可以看成是JavaScript的超集,主要提供了类型系统对ES6的支持所有JavaScript脚本都可以当作TypeScript脚本(但是可能会报错),此外它再增加了一些自己的语法。
  • 类型
    • 类型指一组具有相同特征的值。如果两个值具有某种共同的特征,就可以说它们属于同一种类型。一旦确定某个值的类型,就意味着这个值具有该类型的所有特征,可以进行该类型的所有运算。凡是适用该类型的地方,都可以使用这个值;凡是不适用该类型的地方,使用这个值都会报错。
    • TypeScript 是在开发阶段报错,这样有利于提早发现错误,避免使用时报错。另一方面,函数定义里面加入类型,具有提示作用,可以告诉开发者这个函数怎么用。
  • 动态类型与静态类型
    • 动态类型在运行时才会进行类型检查,这种语言的类型错误往往会导致运行时错误。JavaScript是一门解释型语言,没有编译阶段,所以它是动态类型,不具有很强的约束性。这对于提前发现代码错误非常不利。
// 例一
let x = 1; //值的类型是数值
x = 'hello'; //但是后面可以改成字符串
// 例二
let y = { foo: 1 };
delete y.foo; //这个属性是可以删掉的
y.bar = 2; //还可以新增其他属性
  • 静态类型在编译阶段就能确定每个变量的类型,这种语言的类型错误往往会导致语法错误。TypeScript 在运行前需要先编译为 JavaScript,而在编译阶段就会进行类型检查,所以 它是静态类型。有利于代码的静态分析、代码重构、发现错误、做到语法提示和自动补全、提供代码文档。
● 是添加了类型系统的 JavaScript,适用于任何规模的项目。
● 是一门静态类型、弱类型的语言。
● 是完全兼容 JavaScript 的,它不会修改 JavaScript 运行时的特性。
● 可以编译为 JavaScript,然后运行在浏览器、Node.js 等任何能运行 JavaScript 的环境中。
● 拥有很多编译选项,类型检查的严格程度由你决定。
● 可以和 JavaScript 共存,这意味着 JavaScript 项目能够渐进式的迁移到 TypeScript。
● 增强了编辑器(IDE)的功能,提供了代码补全、接口提示、跳转到定义、代码重构等能力。
● 拥有活跃的社区,大多数常用的第三方库都提供了类型声明。
● 与标准同步发展,符合最新的 ECMAScript 标准(stage 3)。

2、基本用法

■ 类型声明

        TypeScript 代码最明显的特征,就是为 JavaScript 变量加上了类型声明。类型声明的写法,一律为在标识符后面添加“冒号 + 类型”。函数参数和返回值,也是这样来声明类型。

注意:

  • 变量的值应该与声明的类型一致,如果不一致,TypeScript 就会报错。
  • 变量只有赋值后才能使用,否则就会报错。
let foo:string;
console.log(x) // 报错
let foo:string = 123; // 报错
function toString(num:number):string {
  return String(num);
}

        类型声明是可选的,可以加,也可以不加。即使不加类型声明,依然是有效的 TypeScript 代码,只是这时不能保证 TypeScript 会正确推断出类型。

■ 类型推断

        类型声明并不是必需的,TypeScript会在没有明确的指定类型的时候推断出一个类型。后面,如果变量更改为其他类型的值,跟推断的类型不一致,TypeScript 就会报错。TypeScript也可以推断函数的返回值,正因如此,所以函数返回值的类型通常是省略不写的。

        将以前的 JavaScript 项目改为 TypeScript 项目时,可以逐步地为老代码添加类型,即使有些代码没有添加,也不会无法运行。

        如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查。

■ TypeScript 的编译

        JavaScript 的运行环境(浏览器和 Node.js)不认识 TypeScript 代码。因此,TypeScript 项目要想运行,必须先转为 JavaScript 代码,这个代码转换的过程就叫做“编译”(compile)。

        TypeScript 官方没有做运行环境,只提供编译器。编译时,会将类型声明和类型相关的代码全部删除,只留下能运行的 JavaScript 代码,并且不会改变 JavaScript 的运行结果。

        因此,TypeScript 的类型检查只是编译时的类型检查,而不是运行时的类型检查。一旦代码编译为 JavaScript,运行时就不再检查类型了。

■ 值与类型

        “类型”是针对“值”的,可以视为是后者的一个元属性。每一个值在 TypeScript 里面都是有类型的。比如,3是一个值,它的类型是number。

        TypeScript 代码只涉及类型,不涉及值。所有跟“值”相关的处理,都由 JavaScript 完成。

        TypeScript 项目里面,其实存在两种代码,一种是底层的“值代码”( JavaScript 语法),另一种是上层的“类型代码”(TypeScript 的类型语法)。它们是可以分离的,TypeScript 的编译过程,实际上就是把“类型代码”全部拿掉,只保留“值代码”。

        编写 TypeScript 项目时,不要混淆哪些是值代码,哪些是类型代码。

■ TypeScript Playground

        最简单的 TypeScript 使用方法,就是使用官网的在线编译页面,叫做 TypeScript Playground。

        只要打开这个网页,把 TypeScript 代码贴进文本框,它就会在当前页面自动编译出 JavaScript 代码,还可以在浏览器执行编译产物。如果编译报错,它也会给出详细的报错信息。这个页面还具有支持完整的 IDE 支持,可以自动语法提示。此外,它支持把代码片段和编译器设置保存成 URL,分享给他人。

■ tsc 编译器

        TypeScript 官方提供的编译器叫做 tsc,可以将 TypeScript 脚本编译成 JavaScript 脚本。本机想要编译 TypeScript 代码,必须安装 tsc。根据约定,TypeScript 脚本文件使用.ts后缀名,JavaScript 脚本文件使用.js后缀名。tsc 的作用就是把.ts脚本转变成.js脚本。

■ 安装

$ npm install -g typescript # 全局安装。tsc是一个npm模块,必须先安装npm。也可安装为一个依赖模块
# 或者 tsc --version
$ tsc -v # 检查一下是否安装成功
#Version 5.1.6

帮助信息

$ tsc -h # -h或--help参数输出帮助信息
$ tsc --all # 查看完整的帮助信息

编译脚本

$ tsc app.ts # tsc命令后面,加上TypeScript脚本文件,就可以将其编译成JavaScript脚本
$ tsc file1.ts file2.ts file3.ts # 也可以一次编译多个TypeScript脚本

        tsc 有很多参数,可以调整编译行为。

  • --outFile:将多个 TypeScript 脚本编译成一个 JavaScript 文件。
  • --outDir:指定将编译结果保存到其他目录(默认都保存在当前目录)。
  • --target:指定编译后的JavaScript版本。建议使用es2015,或者更新版本。
$ tsc file1.ts file2.ts --outFile app.js # 将多个 TypeScript 脚本编译成一个 JavaScript 文件
$ tsc app.ts --outDir dist # 指定将编译结果保存到其他目录(默认都保存在当前目录)
$ tsc --target es2015 app.ts # 指定编译后的JavaScript版本

编译错误的处理

        编译过程中,如果没有报错,tsc命令不会有任何显示如果编译报错,tsc命令就会显示报错信息,但是这种情况下,依然会编译生成 JavaScript 脚本。因为 TypeScript 团队认为,编译器的作用只是给出编译错误,至于怎么处理这些错误,那就是开发者自己的判断了。开发者更了解自己的代码,所以不管怎样,编译产物都会生成,让开发者决定下一步怎么处理。

# 如果希望一旦报错就停止编译,不生成编译产物,可以使用--noEmitOnError参数。
$ tsc --noEmitOnError app.ts
# 只检查类型是否正确,不生成 JavaScript 文件,使用--noEmit参数
$ tsc --noEmit app.ts

tsconfig.json

        TypeScript 允许将tsc的编译参数,写在配置文件tsconfig.json。只要当前目录有这个文件,tsc就会自动读取,所以运行时可以不写参数。编译时直接调用tsc命令就可以了。

$ tsc file1.ts file2.ts --outFile dist/app.js
# 上面这个命令写成tsconfig.json,就是下面这样
{
  "files": ["file1.ts", "file2.ts"],
  "compilerOptions": {
    "outFile": "dist/app.js"
  }
}
# 有了这个配置文件,编译时直接调用tsc命令就可以了
$ tsc

■ ts-node 模块

        ts-node 是一个非官方的 npm 模块,可以直接运行 TypeScript 代码

$ npm install -g ts-node # 使用时,可以先全局安装它
$ ts-node script.ts # 安装后,就可以直接运行 TypeScript 脚本
$ npx ts-node script.ts # 如果不安装ts-node,也可以通过npx调用它来运行TypeScript脚本
# 如果执行ts-node命令不带有任何参数,它会提供一个TypeScript的命令行REPL运行环境,
# 可以在这个环境中逐行输入TypeScript代码,逐行执行。
$ ts-node
> const twice = (x:string) => x + x;
> twice('abc')
'abcabc'
> # 要退出这个REPL环境,可以按下Ctrl + d,或者输入.exit

3、any 类型、unknown 类型、never 类型

        TypeScript 有两个“顶层类型”(any和unknown),但是“底层类型”只有never唯一一个。never是 TypeScript 的唯一一个底层类型,所有其他类型都包括了never。

■ any 类型

基本含义

        any 类型表示没有任何限制,该类型的变量可以赋予任意类型的值。变量类型一旦设为any,实际上会关闭这个变量的类型检查。即使有明显的类型错误,只要句法正确,都不会报错,怎么使用都可以。

实际开发中,any类型主要适用以下两个场合。

(1)出于特殊原因,需要关闭某些变量的类型检查,就可以把该变量的类型设为any。

(2)为了适配以前老的 JavaScript 项目,让代码快速迁移到 TypeScript,可以把变量类型设为any。有些年代很久的大型 JavaScript 项目,尤其是别人的代码,很难为每一行适配正确的类型,这时为那些类型复杂的变量加上any,TypeScript 编译时就不会报错。

        从集合论的角度看,any类型可以看成是所有其他类型的全集,包含了一切可能的类型。TypeScript 将这种类型称为“顶层类型”,意为涵盖了所有下层。

类型推断问题

        对于开发者没有指定类型、TypeScript必须自己推断类型的那些变量,如果无法推断出类型,就会认为该变量的类型是any,以至于后面就不再对其进行类型检查了,怎么用都可以。对于那些类型不明显的变量,一定要显式声明类型,防止被推断为any。

        TypeScript 提供了一个编译选项noImplicitAny,打开该选项,只要推断出any类型就会报错

$ tsc --noImplicitAny app.ts

        即使开了noImplicitAny,使用let和var命令声明变量,但不赋值也不指定类型,是不会报错的。const命令没有这个问题,因为 JavaScript 语言规定const声明变量时,必须同时进行初始化(赋值)。因此,建议使用let和var声明变量时,如果不赋值,就一定要显式声明类型,否则可能存在安全隐患。

污染问题

        any类型除了关闭类型检查,还有一个很大的问题,就是它会污染其他具有正确类型的变量。它可以赋值给其他任何类型的变量(因为没有类型检查),导致其他变量出错。TypeScript也检查不出错误,问题留到运行时才会暴露。

■ unknown 类型

        为了解决any类型污染其他变量的问题,TypeScript3.0引入了unknown类型。它与any含义相同,表示类型不确定,可能是任意类型,但它有一些使用限制,不像any那样自由。可以视为严格版的、更安全的any。一般来说,凡是需要设为any类型的地方,通常都应该优先考虑设为unknown类型。

(1)unknown跟any的相似之处,在于所有类型的值都可以分配给unknown类型。

(2)unknown类型跟any类型的不同之处,在于它不能直接使用。主要有以下几个限制。

  • unknown类型的变量,不能直接赋值给其他类型的变量(除了any类型和unknown类型)。
  • 不能直接调用unknown类型变量的方法和属性
  • unknown类型变量能够进行的运算是有限的,只能进行比较运算(==、===、!=、!==、||、&&、?)、取反运算(!)、typeof运算符和instanceof运算符这几种,其他运算都会报错。
  • 只有经过“类型缩小”,unknown类型变量才可以使用。就是缩小unknown变量的类型范围,确保不会出错。只有明确unknown变量的实际类型,才允许使用它,防止像any那样可以随意乱用,污染其他变量。

        在集合论上,unknown也可以视为所有其他类型(除了any)的全集,所以它和any一样,也属于 TypeScript 的“顶层类型”。

■ never 类型

        为了保持与集合论的对应关系,以及类型运算的完整性,TypeScript 还引入了“空类型”的概念,即该类型为空,不包含任何值。由于不存在任何属于空类型的值,所以该类型被称为never,即不可能有这样的值。变量的类型是never,就不可能赋给它任何值,否则都会报错。never类型的一个重要特点是,可以赋值给任意其他类型

never类型的使用场景

(1)主要是在一些类型运算之中,保证类型运算的完整性。

(2)不可能返回值的函数,返回值的类型就可以写成never。

(3)如果一个变量可能有多种类型(即联合类型),通常需要使用分支处理每一种类型。这时,处理所有可能的类型之后,剩余的情况就属于never类型。

        在集合论上,空集是任何集合的子集。TypeScript 就相应规定,任何类型都包含了never类型。因此,never类型是任何其他类型所共有的,TypeScript 把这种情况称为“底层类型”。

4、类型系统

■ 基本类型

概述

        JavaScript 语言(注意,不是 TypeScript)将值分成8种类型。

        分别是boolean、string、number、bigint、undefined、null、symbol、object。注意,上面所有类型的名称都是小写字母,首字母大写的Number、String、Boolean等在JavaScript语言中都是内置对象,而不是类型名称。另外,undefined和null既可作为值,也可作为类型,取决于在哪里使用它们。

        TypeScript 继承了 JavaScript 的类型设计,以上8种类型可以看作 TypeScript 的基本类型,是类型系统的基础,复杂类型由它们组合而成。

简单介绍

  • boolean 类型:只包含true和false两个布尔值。
  • string 类型:包含所有字符串。
  • number 类型:包含所有整数和浮点数、非十进制数。
  • bigint 类型:包含所有的大整数。bigint与number类型不兼容。bigint类型赋值为整数和小数,都会报错。
  • symbol 类型:包含所有的 Symbol 值。
  • object 类型:包含了所有对象、数组和函数。
  • undefined 类型,null 类型:两种独立类型,它们各自都只有一个值。
    • undefined 类型只包含一个值undefined,表示未定义(即还未给出定义,以后可能会有定义)。
    • null 类型也只包含一个值null,表示为空(即此处没有值)。
    • 注意:如果没有声明类型的变量,被赋值为undefined或null,在关闭编译设置noImplicitAny和strictNullChecks时,它们的类型会被推断为any。如果希望避免这种情况,则需要打开编译选项strictNullChecks,打开后,赋值为undefined的变量会被推断为undefined类型,赋值为null的变量会被推断为null类型。

■ 包装对象类型

包装对象的概念

        JavaScript 的8种类型之中,undefined和null其实是两个特殊值,object属于复合类型,剩下的五种属于原始类型,代表最基本的、不可再分的值。

        boolean、string、number、bigint、symbol,这五种原始类型的值,都有对应的包装对象。所谓包装对象,指的是这些值在需要时,会自动产生的对象。

  • symbol 类型和 bigint 类型无法直接获取它们的包装对象(即Symbol()和BigInt()不能作为构造函数使用),但是剩下三种可以。
    • 注意:Symbol和BigInt这两个类型虽然存在,但是完全没有使用的理由。目前在 TypeScript 里面,symbol和Symbol两种写法没有差异,bigint和BigInt也是如此。建议始终使用小写的symbol和bigint,不使用大写的Symbol和BigInt。
  • Boolean()、String()、Number()三个构造函数,执行后可直接获取某个原始类型值的包装对象。
    • 注意:String()只有当作构造函数使用时(带有new命令调用),才会返回包装对象。如果当作普通函数使用(不带有new命令),返回就是一个普通字符串。Number()和Boolean()也是如此。

包装对象类型与字面量类型

        由于包装对象的存在,导致每一个原始类型的值都有包装对象和字面量两种情况。

        为了区分这两种情况,TypeScript 对五种原始类型分别提供了大写和小写两种类型。

        Boolean 和 boolean、String 和 string、Number 和 number、BigInt 和 bigint、Symbol 和 symbol。其中,大写类型同时包含包装对象和字面量两种情况,小写类型只包含字面量,不包含包装对象。建议只使用小写类型,不使用大写类型。因为绝大部分使用原始类型的场合,都是使用字面量,不使用包装对象。而且,TypeScript 把很多内置方法的参数,定义成小写类型,使用大写类型会报错。

'hello' // 字面量
new String('hello') // 包装对象
//String类型可以赋值为字符串的字面量,也可以赋值为包装对象。
const s1:String = 'hello'; // 正确
const s2:String = new String('hello'); // 正确
//string类型只能赋值为字面量,赋值为包装对象就会报错。
const s3:string = 'hello'; // 正确
const s4:string = new String('hello'); // 报错

■ Object 类型与 object 类型

Object 类型

        大写的Object类型代表 JavaScript 语言里面的广义对象。所有可以转成对象的值,都是Object类型,这囊括了几乎所有的值。

  • 原始类型值、对象、数组、函数都是合法的Object类型。
  • undefined和null赋值给Object类型,就会报错。
  • 空对象{}是Object类型的简写形式,所以使用Object时常常用空对象代替。

object 类型

        小写的object类型代表 JavaScript 里面的狭义对象。即可以用字面量表示的对象只包含对象、数组和函数,不包括原始类型的值。

        大多数时候,我们使用对象类型,只希望包含真正的对象,不希望包含原始类型。因此,建议总是使用小写类型object,不使用大写类型Object。

        注意:无论是大写的Object类型,还是小写的object类型,都只包含 JavaScript 内置对象原生的属性和方法,用户自定义的属性和方法都不存在于这两个类型之中。

■ undefined 和 null 的特殊性

        undefined和null既是值,又是类型。

        作为值,它们有一个特殊的地方:任何其他类型的变量都可以赋值为undefined或null。以便跟 JavaScript 的行为保持一致(变量如果等于undefined就表示还没有赋值,如果等于null就表示值为空)。

        但是有时候,这并不是开发者想要的行为,也不利于发挥类型系统的优势。

const obj:object = undefined;
obj.toString() // 编译不报错,运行就报错。因为undefined不是对象,没有这个方法

        只要打开strictNullChecks选项,undefined和null就只能赋值给自身,或any类型和unknown类型的变量,不能赋值给其他类型的变量

■ 值类型

        TypeScript 规定,单个值也是一种类型,称为“值类型”。

let x:'hello';
//变量x的类型是字符串hello,导致它只能赋值为这个字符串,赋值为其他字符串就会报错。
x = 'hello'; // 正确
x = 'world'; // 报错

        推断类型时,遇到const命令声明的变量,如果代码里没有注明类型,就会推断该变量是值类型。const命令声明的变量,一旦声明就不能改变,相当于常量。值类型就意味着不能赋为其他值。

        const命令声明的变量,如果赋值为对象,并不会推断为值类型。const变量赋值为对象时,属性值是可以改变的。

        只包含单个值的值类型,用处不大。实际开发中,往往将多个值结合,作为联合类型使用。

const x = 'https'; // x 的类型是 "https"
const y:string = 'https'; // y 的类型是 string
const x = { foo: 1 }; // x 的类型是 { foo: number }

■ 联合类型

        指的是多个类型组成的一个新类型,使用符号|表示。表示取值可以为多种类型中的一种

        联合类型A|B表示,任何一个类型只要属于A或B,就属于联合类型A|B。

        联合类型可以与值类型相结合,表示一个变量的值有若干种可能

let x:string|number;
let rainbowColor:'赤'|'橙'|'黄'|'绿'|'青'|'蓝'|'紫';

        前面提到,打开编译选项strictNullChecks后,其他类型的变量不能赋值为undefined或null。这时,如果某个变量确实可能包含空值,就可以采用联合类型的写法。

let name:string|null; // 变量name的值可以是字符串,也可以是null
name = 'John';
name = null;

        “类型缩小”是 TypeScript 处理联合类型的标准方法,凡是遇到可能为多种类型的场合,都需要先缩小类型,再进行处理。实际上,联合类型本身可以看成是一种类型放大,处理时就需要类型缩小。

function getPort(
  scheme: 'http'|'https'
) {
  switch (scheme) { //对参数变量scheme进行类型缩小,根据不同的值类型,返回不同的结果
    case 'http':
      return 80;
    case 'https':
      return 443;
  }
}

        当TypeScript不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法。联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型。

function getLength(something: string | number): number {
    return something.length; // length 不是 string 和 number 的共有属性,所以会报错
}
// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
//   Property 'length' does not exist on type 'number'.
function getString(something: string | number): string {
    return something.toString(); // 访问 string 和 number 的共有属性是没问题的
}

■ 交叉类型

        指的多个类型组成的一个新类型,使用符号&表示。

        交叉类型A&B表示,任何一个类型必须同时属于A和B,才属于交叉类型A&B,即交叉类型同时满足A和B的特征。

        交叉类型的主要用途是表示对象的合成。常常用来为对象类型添加新属性。

type A = { foo: number };
type B = A & { bar: number }; // 类型B是一个交叉类型,用来在A的基础上增加了属性bar

■ type 命令

        用来定义一个类型的别名。别名可以让类型的名字变得更有意义,也能增加代码的可读性,还可以使复杂类型用起来更方便,便于以后修改变量的类型。类型别名常用于联合类型。

        别名不允许重名

        别名的作用域是块级作用域。这意味着,代码块内部定义的别名,影响不到外部。

        别名支持使用表达式,也可以在定义一个别名时,使用另一个别名,即别名允许嵌套。

        type命令属于类型相关的代码,编译成 JavaScript 的时候,会被全部删除。

        类型别名与字符串字面量类型都是使用 type 进行定义。

type Age = number; // 为number类型定义了一个别名Age
let age:Age = 55; // 像使用number一样,使用Age作为类型
type World = "world";
type Greeting = `hello ${World}`; // 使用了模板字符串,读取另一个别名World
// 使用 type 定了一个字符串字面量类型EventNames,它只能取三种字符串中的一种
type EventNames = 'click' | 'scroll' | 'mousemove';

■ typeof 运算符

        JavaScript中,typeof 运算符是一个一元运算符,返回一个字符串,代表操作数的类型。它的操作数是一个值,typeof运算符只可能返回八种结果,而且都是字符串。

        TypeScript 将typeof运算符移植到了类型运算,它的操作数依然是一个值,但是返回的不是字符串,而是该值的 TypeScript 类型。所以只能用在类型运算中(跟类型相关的代码中),不能用在值运算。

typeof 'foo'; // 'string'
const a = { x: 0 };
type T0 = typeof a;   // { x: number }
type T1 = typeof a.x; // number

        同一段代码可能存在两种typeof运算符,一种用在值相关的 JavaScript 代码部分,另一种用在类型相关的 TypeScript 代码部分。它们遵守各自的规则,且编译后,前者会保留,后者会被全部删除。

        由于编译时不会进行 JavaScript 的值运算,所以TypeScript 规定,typeof 的参数只能是标识符,不能是需要运算的表达式

        typeof命令的参数不能是类型,也不能是类型别名

■ 块级类型声明

        类型可以声明在代码块(用大括号表示)里面,并且只在当前代码块有效,在代码块外部无效。

■ 类型的兼容

        如果类型A的值可以赋值给类型B,那么类型A就称为类型B的子类型。

        TypeScript 的一个规则是,凡是可以使用父类型的地方,都可以使用子类型,但是反过来不行。因为子类型继承了父类型的所有特征,所以可以用在父类型的场合。但是,子类型还可能有一些父类型没有的特征,所以父类型不能用在子类型的场合。

5、数组

■ 简介

        TypeScript 数组有一个根本特征:所有成员的类型必须相同,但是成员数量是不确定的可以是无限数量的成员,也可以是零成员。数组的项中不允许出现其他的类型。

数组的类型有两种写法:

  • 第一种写法是「类型 + 方括号」表示法。数组成员的类型后面,加上一对方括号
    • 如果数组成员的类型比较复杂,可以写在圆括号里面。
    • 如果数组成员可以是任意类型,写成any[]。当然,这种写法是应该避免的。
let arr:number[] = [1, 2, 3];
let arr:(number|string)[]; // 圆括号是必须的,否则因为竖杠|的优先级低于[],会把number|string[]理解成number和string[]的联合类型
let arr:any[];
  • 第二种写法是「数组泛型 Array<elemType>」。使用TypeScript内置的Array接口。这种写法对于成员类型比较复杂的数组,代码可读性会稍微好一些。
let arr:Array<number> = [1, 2, 3];
let arr:Array<number|string>; // 这种写法本质上属于泛型,这里只要知道怎么写就可以了

        数组类型声明了以后,成员数量是不限制的,任意数量的成员都可以,也可以是空数组。

        数组的成员是可以动态变化的。所以TS不会对数组边界进行检查,越界访问数组并不会报错

        TypeScript 允许使用方括号读取数组成员的类型

let arr:number[];
arr = [];
arr = [1]; // 数组arr无论有多少个成员,都是正确的------------------------------------
let arr:number[] = [1, 2, 3];
arr[3] = 4;
arr.length = 2;
arr // [1, 2] 数组增加成员或减少成员,都是可以的。------------------------------------
let arr:number[] = [1, 2, 3];
let foo = arr[3]; // 正确 变量foo的值是一个不存在的数组成员,TypeScript并不会报错-----
type Names = string[];
type Name = Names[0]; // string
type Name = Names[number]; // string 数组成员的索引类型都是number,所以读取成员类型也可这样写

■ 数组的类型推断

        如果数组变量没有声明类型,TypeScript就会推断数组成员的类型。这时,推断行为会因为值的不同,而有所不同。

  • 如果变量的初始值是空数组,那么 TypeScript 会推断数组类型是any[]。
  • 后面,为这个数组赋值时,TypeScript 会自动更新类型推断。
const arr = [];
arr // 推断为 any[]
arr.push(123);
arr // 推断类型为 number[]
arr.push('abc');
arr // 推断类型为 (string|number)[]
  • 但是,类型推断的自动更新只发生在初始值为空数组的情况。如果初始值不是空数组,类型推断就不会更新。
const arr = [123]; // 推断类型为 number[] 推断成员类型为number
arr.push('abc'); // 报错 新成员如果不是这个类型,就会报错,而不会更新类型推断

■ 只读数组,const 断言

        JavaScript规定,const命令声明的数组变量是可以改变成员的。

        但是,很多时候确实有声明为只读数组的需求,即不允许变动数组成员。TypeScript允许声明只读数组,方法是在数组类型前面加上readonly关键字。

        由于只读数组是数组的父类型,所以它不能代替数组。

const arr:readonly number[] = [0, 1];
delete arr[0]; // 报错。arr是一个只读数组,删除、修改、新增数组成员都会报错-----------
// readonly number[]与number[]视为两种不一样的类型,后者是前者的子类型
let a1:number[] = [0, 1];
let a2:readonly number[] = a1; // 正确。子类型number[]可以赋值给父类型readonly number[]
a1 = a2; // 报错。但是反过来就会报错

        注意,readonly关键字不能与数组的泛型写法一起使用。

        实际上,TypeScript 提供了两个专门的泛型,用来生成只读数组的类型。

        泛型ReadonlyArray<T>和Readonly<T[]>都可以用来生成只读数组类型。两者尖括号里面的写法不一样,Readonly<T[]>的尖括号里面是整个数组(number[]),而ReadonlyArray<T>的尖括号里面是数组成员(number)

const arr:readonly Array<number> = [0, 1];// 报错
const a1:ReadonlyArray<number> = [0, 1];
const a2:Readonly<number[]> = [0, 1];

        只读数组还有一种声明方法,就是使用“const 断言”

const arr = [0, 1] as const; //as const告诉TS,推断类型时要把变量arr推断为只读数组,数组成员无法改变
arr[0] = [2]; // 报错 

■ 多维数组

        TypeScript 使用T[][]的形式,表示二维数组,T是最底层数组成员的类型。

var multi:number[][] = [[1,2,3], [23,24,25]]; //表示一个二维数组,最底层数组成员类型是number

■ any 在数组中的应用

        一个比较常见的做法是,用 any 表示数组中允许出现任意类型

let list: any[] = ['xcatliu', 25, { website: 'http://xcatliu.com' }];

6、元组

■ 简介

        元组是TypeScript特有的数据类型,JavaScript没有单独区分这种类型。它表示成员类型可以自由设置的数组,即数组的各个成员的类型可以不同

        由于成员的类型可以不一样,所以元组必须明确声明每个成员的类型不能省略,否则TypeScript会把一个值自动推断为数组。

const s:[string, string, boolean] = ['a', 'b', true];
// a 的类型被推断为 (number | boolean)[]
let a = [1, true]; //变量a的值其实是一个元组,但是TS会将其推断为一个联合类型的数组

        数组的成员类型写在方括号外面(number[]),元组的成员类型是写在方括号里面([number])。TypeScript 的区分方法就是,成员类型写在方括号里面的就是元组,写在外面的就是数组。

// 数组
let a:number[] = [1];
// 元组
let t:[number] = [1];

        元组成员的类型可以添加问号后缀(?),表示该成员是可选的。注意,问号只能用于元组的尾部成员,也就是说,所有可选成员必须在必选成员之后

let a:[number, number?, string?] = [1];

        由于需要声明每个成员的类型,所以大多数情况下,元组的成员数量是有限的,从类型声明就可以明确知道,元组包含多少个成员,越界的成员会报错。

let x:[string, string] = ['a', 'b'];
x[2] = 'c'; // 报错。变量x是一个只有两个成员的元组,如果对第三个成员赋值就报错了

        越界的元素:当添加越界的元素时,它的类型会被限制为元组中每个类型的联合类型

let tom: [string, number];
tom = ['Tom', 25];
tom.push('male');
tom.push(true);
// Argument of type 'true' is not assignable to parameter of type 'string | number'.

        但是,使用扩展运算符(...),可以表示不限成员数量的元组。扩展运算符(...)用在元组的任意位置都可以,它的后面只能是一个数组或元组。

type t1 = [string, number, ...boolean[]];
type t2 = [string, ...boolean[], number];
type t3 = [...boolean[], string, number];

        如果不确定元组成员的类型和数量,可以写成下面这样。

type Tuple = [...any[]];

        元组的成员可以添加成员名,这个成员名是说明性的,可以任意取名,没有实际作用。

type Color = [ //每个成员都有一个名字,没有实际作用,只是用来说明每个成员的含义
  red: number,
  green: number,
  blue: number
];
const c:Color = [255, 255, 255];

        元组可以通过方括号,读取成员类型。

type Tuple = [string, number, Date]; 
type Age = Tuple[1]; // number 返回1号位置的成员类型
//由于元组的成员都是数值索引,即索引类型都是number,所以可以像下面这样读取
type TupleEl = Tuple[number];  // string|number|Date 表示元组的所有数值索引的成员类型
当赋值或访问一个已知索引的元素时,会得到正确的类型。
可以只赋值其中一项,但是当直接对元组类型的变量进行初始化或者赋值的时候,需要提供所有元组类型中指定的项。

■ 只读元组

        元组也可以是只读的,不允许修改,有两种写法。

// 写法一
type t = readonly [number, string]
// 写法二 一个泛型,用到了工具类型Readonly<T>
type t = Readonly<[number, string]> 

        只读元组是元组的父类型。元组可以替代只读元组,而只读元组不能替代元组。

■ 成员数量的推断

        如果没有可选成员和扩展运算符,TypeScript 会推断出元组的成员数量(即元组长度)。

        如果包含了可选成员,TypeScript会推断出可能的成员数量

        如果使用了扩展运算符,TypeScript 就无法推断出成员数量。此时,TypeScript内部就会把该元组当成数组处理。

function f(
  point:[number, number?, number?]
) {
  if (point.length === 4) {  // 报错
    // ...
  }
} // TypeScript发现point.length的类型是1|2|3,不可能等于4--------------------------
const myTuple:[...string[]]
  = ['a', 'b', 'c'];

if (myTuple.length === 4) { // 正确
  // ...
} // 用到了扩展运算符,TypeScript 把myTuple当成数组看待,而数组的成员数量是不确定的-----

■ 扩展运算符与成员数量

        扩展运算符(...)将数组(注意,不是元组)转换成一个逗号分隔的序列,这时TypeScript会认为这个序列的成员数量是不确定的,因为数组的成员数量是不确定的。

        导致如果函数调用时,使用扩展运算符传入函数参数,可能发生参数数量与数组长度不匹配的报错。

const arr = [1, 2];
function add(x:number, y:number){ //函数add()只能接受两个参数
  // ...
}
add(...arr) // 报错。传入的是...arr,TypeScript认为转换后的参数个数是不确定的

        解决这个问题的一个方法,就是把成员数量不确定的数组,写成成员数量确定的元组,再使用扩展运算符。

const arr:[number, number] = [1, 2]; //arr是一个拥有两个成员的元组
function add(x:number, y:number){
  // ...
}
add(...arr) // 正确。TypeScript能够确定...arr可以匹配函数add()的参数数量,就不会报错了

        另一种写法是使用as const断言

const arr = [1, 2] as const; //这是一个只读的值类型,可以当作数组,也可以当作元组

        有些函数可以接受任意数量的参数,这时使用扩展运算符就不会报错。

const arr = [1, 2, 3];
console.log(...arr) // 正确。console.log()可接受任意数量的参数,传入...arr不会报错

7、symbol 类型

■ 简介

        类似于字符串,但是每一个Symbol值都是独一无二的,与其他任何值都不相等。Symbol 值通过Symbol()函数生成。在 TypeScript 里面,Symbol 的类型使用symbol表示。

let x:symbol = Symbol();
let y:symbol = Symbol();
x === y // false。变量x和y的类型都是symbol,且都用Symbol()生成,但是它们是不相等的

■ unique symbol

        symbol类型包含所有的Symbol值,但是无法表示某一个具体的Symbol值。Symbol值不存在字面量,必须通过变量来引用,所以写不出只包含单个Symbol值的那种值类型。为了解决这个问题,

        TypeScript设计了symbol的一个子类型unique symbol,它表示单个的、某个具体的 Symbol 值。因为unique symbol表示单个值,所以这个类型的变量是不能修改值的,只能用const命令声明,不能用let声明。const命令为变量赋值Symbol值时,变量类型默认就是unique symbol,所以类型可以省略不写。

const x:unique symbol = Symbol();// 正确
let y:unique symbol = Symbol();// 报错
const x:unique symbol = Symbol();
// 等同于
const x = Symbol();

        每个声明为unique symbol类型的变量,它们的值都是不一样的,其实属于两个值类型。

const a:unique symbol = Symbol();
const b:unique symbol = Symbol();
a === b // 报错。变量a和变量b都是unique symbol,但其实是两个值类型,不同类型的值肯定是不相等的
const a:'hello' = 'hello';
const b:'world' = 'world';
a === b // 报错。变量a和b都是字符串,但属于不同的值类型,不能使用严格相等运算符进行比较

        unique symbol 类型是 symbol 类型的子类型所以可以将前者赋值给后者,但是反过来就不行。

        unique symbol 类型的一个作用,就是用作属性名,这可以保证不会跟其他属性名冲突。如果要把某一个特定的 Symbol 值当作属性名,那么它的类型只能是 unique symbol,不能是 symbol

        unique symbol类型也可以用作类(class)的属性值,但只能赋值给类的readonly static属性

■ 类型推断

        如果变量声明时没有给出类型,TypeScript会推断某个Symbol值变量的类型。

        let命令声明的变量,推断类型为symbol。此时,如果赋值为另一个unique symbol类型的变量,则推断类型还是 symbol

let x = Symbol(); // 类型为 symbol
const x = Symbol();
let y = x;// 类型为 symbol

        const命令声明的变量,推断类型为unique symbol。此时,如果赋值为另一个symbol类型的变量,则推断类型为symbol

const x = Symbol(); // 类型为 unique symbol
let x = Symbol();
const y = x; // 类型为 symbol

8、函数

■ 简介

        函数的类型声明,需要在声明函数时,给出参数的类型和返回值的类型

        如果不指定参数类型,TS就会推断参数类型,如果缺乏足够信息,就会推断该参数的类型为any。

        返回值的类型通常可以不写,因为TypeScript自己会推断出来。有时候出于文档目的,或者为了防止不小心改掉返回值,还是会写返回值的类型。

        函数类型里面的参数名与实际参数名,可以不一致。函数的实际参数个数,可以少于类型指定的参数个数,但是不能多于,即TypeScript允许省略参数

        使用函数表达式声明函数,如果变量被赋值为一个函数,变量的类型有两种写法。

// 写法一
const hello = function (txt:string) {
  console.log('hello ' + txt);
}
// 写法二
const hello:
  (txt:string) => void
= function (txt) {
  console.log('hello ' + txt);
};

        如果函数的类型定义很冗长,或者多个函数使用同一种类型,写法二用起来就很麻烦。因此,往往用type命令为函数类型定义一个别名,便于指定给其他变量。

type MyFunc = (txt:string) => void; // 为函数类型定义了一个别名MyFunc
const hello:MyFunc = function (txt) {
  console.log('hello ' + txt);
};

        如果一个变量要套用另一个函数类型,有一个小技巧,就是使用typeof运算符。任何需要类型的地方,都可以使用typeof运算符从一个值获取类型。

function add(
  x:number,
  y:number
) {
  return x + y;
}
// 函数myAdd()的类型与函数add()是一样的,那么就可以定义成typeof add,因为函数名add本身不是类型,而是一个值,所以要用typeof运算符返回它的类型
const myAdd:typeof add = function (x, y) {
  return x + y;
}

        函数类型还可以采用对象的写法。注意,这种写法的函数参数与返回值之间,间隔符是冒号:,而不是正常写法的箭头=>,因为这里采用的是对象类型的写法,对象的属性名与属性值之间使用的是冒号。这种写法平时很少用,但是非常合适用在一个场合:函数本身存在属性。

let foo: {
  (x:number): void;
  version: string // 函数f()本身还有一个属性version
} = f;

        函数类型可使用Interface来声明,定义一个函数需要符合的形状,这种写法就是对象写法的翻版。

interface myfn { // 定义了接口myfn
  (a:number, b:number): number; // 这个接口的类型就是一个用对象表示的函数
}
var add:myfn = (a, b) => a + b;

■ Function 类型

        TypeScript 提供 Function 类型表示函数,任何函数都属于这个类型。

        Function 类型的值都可以直接执行。Function 类型的函数可以接受任意数量的参数,每个参数的类型都是any,返回值的类型也是any,代表没有任何约束,所以不建议使用这个类型,给出函数详细的类型声明会更好。

function doSomething(f:Function) {
  return f(1, 2, 3);
}

■ 箭头函数

        箭头函数是普通函数的一种简化写法,它的类型写法与普通函数类似。类型声明写在箭头函数的定义里面时,参数的类型写在参数名后面,返回值类型写在参数列表的圆括号后面。注意,类型写在箭头函数的定义里面,与使用箭头函数表示函数类型,写法有所不同

const repeat = ( //变量repeat被赋值为一个箭头函数,类型声明写在箭头函数的定义里面
  str:string,
  times:number
):string => str.repeat(times); // 类型写在箭头函数的定义里面-----------------------
function greet(
  fn:(a:string) => void //函数greet()的参数fn是一个函数,类型就用箭头函数表示
):void {
  fn('world');
} // 使用箭头函数表示函数类型------------------------------------------------------
type Person = { name: string }; // 一个类型别名,代表一个对象,该对象有属性name
const people = ['alice', 'bob', 'jan'].map( //people是数组的map()方法的返回值
  (name):Person => ({name}) // 返回一个对象,类型为Person,该对象有一个属性name
);

■ 可选参数

        如果函数的某个参数可以省略,则在参数名后面加问号表示。参数名带有问号,表示该参数的类型实际上是原始类型|undefined,它有可能为undefined。但反过来就不成立,类型显式设为undefined的参数,就不能省略。

        函数的可选参数只能在参数列表的尾部,跟在必选参数的后面。如果前部参数有可能为空,这时只能显式注明该参数类型可能为undefined。

        函数体内部用到可选参数时,需要判断该参数是否为undefined。

let myFunc: // 参数a可能为空,只能显式注明类型包括undefined,传参时也要显式传入undefined
  (
    a:number|undefined,
    b:number
  ) => number;
let myFunc: // 第二个参数为可选参数,所以函数体内部需要判断一下,该参数是否为空
  (a:number, b?:number) => number; 
myFunc = function (x, y) {
  if (y === undefined) {
    return x;
  }
  return x + y;
}

■ 参数默认值

        TypeScript会将添加了默认值的参数识别为可选参数。如果不传入该参数,或者传入undefined,就会等于默认值。此时就不受「可选参数必须接在必需参数后面」的限制了。

        可选参数与默认值不能同时使用

        具有默认值的参数如果不位于参数列表的末尾,调用时不能省略,如果要触发默认值,必须显式传入undefined。

function add(
  x:number = 0,
  y:number
) {
  return x + y;
}
add(1) // 报错
add(undefined, 1) // 正确

■ 参数解构

        函数参数如果存在变量解构,类型写法如下。

function f(
  [x, y]: [number, number]
) {
  // ...
}
function sum(
  { a, b, c }: {
     a: number;
     b: number;
     c: number
  }
) {
  console.log(a + b + c);
}

        参数解构可以结合类型别名(type 命令)一起使用,代码会看起来简洁一些。

type ABC = { a:number; b:number; c:number };
function sum({ a, b, c }:ABC) {
  console.log(a + b + c);
}

■ rest 参数

        rest 参数表示函数剩余的所有参数,它可以是数组(剩余参数类型相同),也可能是元组(剩余参数类型不同)。rest 参数只能是最后一个参数

        注意,元组需要声明每一个剩余参数的类型。如果元组里面的参数是可选的,则要使用可选参数。

        rest 参数可以嵌套

        rest 参数可以与变量解构结合使用

// rest 参数为数组
function multiply(n:number, ...m:number[]) {
  return m.map((x) => n * x); // 参数m就是rest类型,它的类型是一个数组
}
// rest 参数为元组
function f(
  ...args: [boolean, string?]
) {}
// rest 参数甚至可以嵌套。
function f(...args:[boolean, ...string[]]) {
  // ...
}
function repeat(
  ...[str, times]: [string, number]
):string {
  return str.repeat(times);
}
// 等同于
function repeat(
  str: string,
  times: number
):string {
  return str.repeat(times);
}

■ readonly 只读参数

        如果函数内部不能修改某个参数,可以在函数定义时,在参数类型前面加上readonly关键字,表示这是只读参数。注意,readonly关键字目前只允许用在数组和元组类型的参数前面,如果用在其他类型的参数前面,就会报错。

function arraySum(
  arr:readonly number[] //表示为只读参数
) {
  // ...
  arr[0] = 0; // 报错。如果函数体内部修改这个数组,就会报错
}

■ void 类型

        void 类型表示函数没有返回值。void 类型允许返回undefined或null。如果返回其他值,就会报错。如果打开了strictNullChecks编译选项,那么 void 类型只允许返回undefined。如果返回null,就会报错。这是因为 JavaScript 规定,如果函数没有返回值,就等同于返回undefined。

// 打开编译选项 strictNullChecks
function f():void {
  return undefined; // 正确
}
function f():void {
  return null; // 报错
}

        注意,如果变量、对象方法、函数参数是一个返回值为 void 类型的函数,那么并不代表不能赋值为有返回值的函数。相反,该变量、对象方法和函数参数可以接受返回任意值的函数,这时并不会报错。这是因为,这时TypeScript认为,这里的 void 类型只是表示该函数的返回值没有利用价值,或者说不应该使用该函数的返回值。只要不用到这里的返回值,就不会报错。

const src = [1, 2, 3];
const ret = [];
src.forEach(el => ret.push(el)); //push()返回插入后数组的长度,对forEach()来说,这个没用
注意,这种情况仅限于变量、对象方法和函数参数,函数字面量如果声明了返回值是 void 类型,还是不能有返回值。

        函数的运行结果如果是抛出错误,也允许将返回值写成void。

        除了函数,其他变量声明为void类型没有多大意义,因为这时只能赋值为undefined或者null(假定没有打开strictNullChecks) 。

■ never 类型

        never类型表示肯定不会出现的值。它用在函数的返回值,就表示某个函数肯定不会返回值,即函数不会正常执行结束

它主要有以下两种情况。

  • 抛出错误的函数。注意,只有抛出错误,才是 never 类型。如果显式用return语句返回一个 Error 对象,返回值就不是 never 类型。由于抛出错误的情况属于never类型或void类型,所以无法从返回值类型中获知,抛出的是哪一种错误。
  • 无限执行的函数。注意,never类型不同于void类型。前者表示函数没有执行结束,不可能有返回值;后者表示函数正常执行结束,但是不返回值,或者说返回undefined。

        如果一个函数抛出了异常或者陷入了死循环,那么该函数无法正常返回一个值,因此该函数的返回值类型就是never。如果程序中调用了一个返回值类型为never的函数,那么就意味着程序会在该函数的调用位置终止,永远不会继续执行后续的代码。

注意,有些函数虽然没有return语句,但实际上是省略了return undefined这行语句,真实的返回值是undefined。

        一个函数如果某些条件下有正常返回值,另一些条件下抛出错误,这时它的返回值类型可以省略never。函数的返回值无论是什么类型,都可能包含了抛出错误的情况。

function sometimesThrow():number { //返回值其实是number|never
  if (Math.random() > 0.5) {
    return 100;
  }

  throw new Error('Something went wrong');
}
const result = sometimesThrow(); //推断为number

■ 局部类型

        函数内部允许声明其他类型,该类型只在函数内部有效,称为局部类型。

function hello(txt:string) {
  type message = string; //在函数hello()内部定义的,只能在函数内部使用
  let newTxt:message = 'hello ' + txt;
  return newTxt;
}

const newTxt:message = hello('world'); // 报错

■ 高阶函数

        一个函数的返回值还是一个函数,那么前一个函数就称为高阶函数(higher-order function)。

(someValue: number) => (multiplier: number) => someValue * multiplier;

■ 函数重载

        有些函数可以接受不同类型或不同个数的参数,并且根据参数的不同,会有不同的函数行为。这种根据参数类型不同,执行不同逻辑的行为,称为函数重载(function overload)。

        TypeScript 对于“函数重载”的类型声明方法是,逐一定义每一种情况的类型

function add(
  x:number,
  y:number
):number;
function add(
  x:any[],
  y:any[]
):any[];
function add( // 函数本身的类型声明,必须与前面已有的重载声明兼容。
  x:number|any[],
  y:number|any[]
):number|any[] {
  if (typeof x === 'number' && typeof y === 'number') {
    return x + y;
  } else if (Array.isArray(x) && Array.isArray(y)) {
    return [...x, ...y];
  }
  throw new Error('wrong parameters');
} // 参数类型和返回值类型都是number|any[],但不意味着参数类型为number时返回值类型为any[]
TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。

        注意,重载的各个类型描述与函数的具体实现之间,不能有其他代码,否则报错。虽然函数的具体实现里面有完整的类型声明,但是函数实际调用的类型,以前面的类型声明为准

        函数重载的每个类型声明之间,以及类型声明与函数实现的类型之间,不能有冲突

        重载声明的排序很重要,因为 TypeScript 是按照顺序进行检查的,一旦发现符合某个类型声明,就不再往下检查了,所以类型最宽的声明应该放在最后面,防止覆盖其他类型声明。

        对象的方法也可以使用重载。

        函数重载也可以用来精确描述函数参数与返回值之间的对应关系

type CreateElement = {
  (tag:'a'): HTMLAnchorElement;
  (tag:'canvas'): HTMLCanvasElement;
  (tag:'table'): HTMLTableElement;
  (tag:string): HTMLElement;
}

        由于重载是一种比较复杂的类型声明方法,为了降低复杂性,一般来说,如果可以的话,应该优先使用联合类型替代函数重载,除非多个参数之间、或者某个参数与返回值之间,存在对应关系。

// 写法一 函数重载
function len(s:string):number;
function len(arr:any[]):number;
function len(x:any):number {
  return x.length;
}
// 写法二 联合类型
function len(x:any[]|string):number {
  return x.length;
}
函数的合并:可以使用重载定义多个函数类型。

■ 构造函数

        JavaScript 语言使用构造函数生成对象的实例。类(class)本质上是构造函数。构造函数的最大特点,就是必须使用new命令调用。构造函数的类型写法,一种是在参数列表前面加上new命令,另一种是采用对象形式

type F = {
  new (s:string): object;
};

        某些函数既是构造函数,又可以当作普通函数使用,比如Date()。

type F = {
  new (s:string): object;
  (n?:number): number;
} // F 既可以当作普通函数执行,也可以当作构造函数使用

9、对象

■ 简介

        对象类型的最简单声明方法,就是使用大括号表示对象,在大括号内部声明每个属性和方法的类型。属性的类型可以用分号结尾,也可以用逗号结尾,最后一个属性后面,可以写分号或逗号,也可以不写。

        一旦声明了类型,对象赋值时,就不能缺少指定的属性,也不能有多余的属性。读写不存在的属性也会报错。不能删除类型声明中存在的属性,修改属性值是可以的。

// 属性类型以分号结尾
type MyObj = {
  x:number;
  y:number; 
};
// 属性类型以逗号结尾
type MyObj = {
  x:number,
  y:number,
};
const o1:MyObj = { x: 1 }; // 报错。变量o1缺少了属性y
const o2:MyObj = { x: 1, y: 1, z: 1 }; // 报错。变量o2多出了属性z

        对象的方法使用函数类型描述。

const obj:{
  x: number;
  y: number;
  add(x:number, y:number): number;
  // 或者写成
  // add: (x:number, y:number) => number;
} = {
  x: 1,
  y: 1,
  add(x, y) {
    return x + y;
  }
};

        对象类型可以使用方括号读取属性的类型。

type User = {
  name: string,
  age: number
};
type Name = User['name']; // string

        除了type命令可以为对象类型声明一个别名,TypeScript还提供了interface命令,可以把对象类型提炼为一个接口

// 写法一 type命令的用法
type MyObj = {
  x:number;
  y:number;
};
const obj:MyObj = { x: 1, y: 1 };
// 写法二 interface命令的用法
interface MyObj {
  x: number;
  y: number;
}
const obj:MyObj = { x: 1, y: 1 };

        注意,TypeScript不区分对象自身的属性和继承的属性,一律视为对象的属性。

interface MyInterface {
  toString(): string; // 继承的属性
  prop: number; // 自身的属性
}
const obj:MyInterface = { // 正确
  prop: 123, // 只写了prop属性,但不报错,因为它可以继承原型上面的toString()方法
};

■ 可选属性

        如果某个属性是可选的(即可以忽略),需要在属性名后面加一个问号。可选属性等同于允许赋值为undefined。

type User = {
  firstName: string;
  lastName?: string;
};
// 等同于
type User = {
  firstName: string;
  lastName?: string|undefined;
};

        读取一个没有赋值的可选属性时,返回undefined。因此,读取可选属性之前,必须检查一下是否为undefined。

// 写法一 使用三元运算符?:,判断是否为undefined,并设置默认值
let firstName = (user.firstName === undefined)
  ? 'Foo' : user.firstName;
let lastName = (user.lastName === undefined)
  ? 'Bar' : user.lastName;
// 写法二 使用 Null 判断运算符??
let firstName = user.firstName ?? 'Foo';
let lastName = user.lastName ?? 'Bar';

        TS提供编译设置ExactOptionalPropertyTypes,只要同时打开这个设置和strictNullChecks,可选属性就不能设为undefined

        注意,可选属性与允许设为undefined的必选属性是不等价的。

type A = { x:number, y?:number }; //一个可选属性,可以省略不写
type B = { x:number, y:number|undefined }; //允许设为undefined的必选属性,省略会报错
const ObjA:A = { x: 1 }; // 正确
const ObjB:B = { x: 1 }; // 报错

■ 只读属性

        属性名前面加上readonly关键字,表示这个属性是只读属性,不能修改。只读属性只能在对象初始化期间赋值,此后就不能修改该属性。注意,如果属性值是一个对象,readonly修饰符并不禁止修改该对象的属性,只是禁止完全替换掉该对象。

        如果一个对象有两个引用,即两个变量对应同一个对象,其中一个变量是可写的,另一个变量是只读的,那么从可写变量修改属性,会影响到只读变量。

        如果希望属性值是只读的,除了声明时加上readonly关键字,还有一种方法,就是在赋值时,在对象后面加上只读断言as const

■ 属性名的索引类型

        TypeScript允许采用属性名表达式的写法来描述类型,称为“属性名的索引类型”。

//索引类型里面,最常见的就是属性名的字符串索引。
type MyObj = { // 属性名类型采用了表达式形式,写在方括号里面
  [property: string]: string // 指定所有名称为字符串的属性,属性值也必须是字符串
};
const obj:MyObj = {
  foo: 'a',
  bar: 'b',
  baz: 'c',
};

        JavaScript 对象的属性名的类型有三种可能,除了string,还有number和symbol。

        对象可以同时有多种类型的属性名索引,比如同时有数值索引和字符串索引。但是,数值索引不能与字符串索引发生冲突,必须服从后者,这是因为在JavaScript语言内部,所有的数值属性名都会自动转为字符串属性名

type MyType = { // 同时有两种属性名索引
  [x: number]: boolean; // 报错
  [x: string]: string;
} // 由于字符属性名的值类型是string,数值属性名的值类型只有同样为string,才不会报错

        同样地,可以既声明属性名索引,也声明具体的单个属性名。如果单个属性名不符合属性名索引的范围,两者发生冲突,就会报错。

type MyType = {
  foo: boolean; // 报错
  [x: string]: string;
} // 属性名foo符合属性名的字符串索引,但是两者的属性值类型不一样,所以报错
属性的索引类型写法,建议谨慎使用,因为 属性名的声明太宽泛,约束太少。另外,属性名的数值索引 不宜用来声明数组,因为采用这种方式声明数组,就 不能使用各种数组方法以及length属性,因为类型里面没有定义这些东西。

■ 解构赋值

        解构赋值用于直接从对象中提取属性。

const {id, name, price}:{
  id: string;
  name: string;
  price: number
} = product; // 从对象product提取了三个属性,并声明属性名的同名变量

        注意,目前没法为解构变量指定类型,因为对象解构里面的冒号,很像是为变量指定类型,其实是为对应的属性指定新的变量名

let { x: foo, y: bar } = obj; //冒号不是表示属性x和y的类型,而是为这两个属性指定新的变量名
// 等同于
let foo = obj.x;
let bar = obj.y;
//如果要为x和y指定类型,不得不写成下面这样
let { x: foo, y: bar }
  : { x: string; y: number } = obj;

■ 结构类型原则

        只要对象B满足对象A的结构特征,TypeScript就认为对象B兼容对象A的类型,这称为结构类型原则(structural typing)。只要可以使用A的地方,就可以使用B。

type A = {
  x: number;
};
type B = { // B满足A的结构特征,B可以赋值给A
  x: number;
  y: number;
};
const B = {
  x: 1,
  y: 1
};
const A:{ x: number } = B; // 正确

        根据结构类型原则,TypeScript检查某个值是否符合指定类型时,并不是检查这个值的类型名(即名义类型),而是检查这个值的结构是否符合要求(即结构类型)。JavaScript 并不关心对象是否严格相似,只要某个对象具有所要求的属性,就可以正确运行。

如果类型B可以赋值给类型A,TypeScript就认为B是A的子类型,A是B的父类型。子类型满足父类型的所有结构特征,同时还具有自己的特征。凡是可以使用父类型的地方,都可以使用子类型,即 子类型兼容父类型。

■ 严格字面量检查

        如果对象使用字面量表示,会触发TypeScript的严格字面量检查。如果字面量的结构跟类型定义的不一样(比如多出了未定义的属性),就会报错。

const point:{
  x:number;
  y:number;
} = { // 等号右边是一个对象的字面量,会触发严格字面量检查,有类型声明中不存在的属性z,导致报错
  x: 1,
  y: 1,
  z: 1 // 报错
};
const myPoint = {
  x: 1,
  y: 1,
  z: 1
};
const point:{
  x:number;
  y:number;
} = myPoint; // 正确。等号右边是一个变量,就不会触发严格字面量检查,从而不报错

        TypeScript对字面量进行严格检查的目的,主要是防止拼写错误。一般来说,字面量大多数来自手写,容易出现拼写错误,或者误用 API。

type Options = {
  title:string;
  darkMode?:boolean; //如果没有严格字面量规则,就不会报错,因为darkMode是可选属性
};
const obj:Options = { //根据结构类型原则,任何对象只要有title属性,都认为符合Options类型
  title: '我的网页',
  darkmode: true, // 报错
};

        规避严格字面量检查,可以使用中间变量

let myOptions = {
  title: '我的网页',
  darkmode: true,
};
const obj:Options = myOptions; //这时变量obj的赋值,不属于直接字面量赋值

        如果确认字面量没有错误,也可以使用类型断言as Options规避严格字面量检查。告诉编译器,字面量符合Options类型,就能规避这条规则。

const obj:Options = {
  title: '我的网页',
  darkmode: true,
} as Options; //使用类型断言as Options告诉编译器,字面量符合Options类型就能规避这条规则

        如果允许字面量有多余属性,可以在类型里面定义一个通用属性

let x: {
  foo: number,
  [x: string]: any //字符串索引([x: string])导致任何字符串属性名都是合法的
};
x = { foo: 1, baz: 2 };  // Ok

        由于严格字面量检查,字面量对象传入函数必须很小心,不能有多余的属性

        编译器选项suppressExcessPropertyErrors,可以关闭多余属性检查。下面是它在 tsconfig.json 文件里面的写法。

{
  "compilerOptions": {
    "suppressExcessPropertyErrors": true
  }
}

■ 最小可选属性规则

        根据结构类型原则,如果一个对象的所有属性都是可选的,那么其他对象跟它都是结构类似的。为了避免这种情况,TypeScript 2.4引入了一个最小可选属性规则,也称为弱类型检测。

        如果某个类型的所有属性都是可选的,那么该类型的对象必须至少存在一个可选属性,不能所有可选属性都不存在,这就叫做“最小可选属性规则”。

type Options = {
  a?:number;
  b?:number;
  c?:number;
};
const opts = { d: 123 };
const obj:Options = opts; // 报错。对象opts与类型Options没有共同属性,赋值给该类型的变量就会报错

        如果想规避这条规则,要么在类型里面增加一条索引属性([propName: string]: someType),要么使用类型断言(opts as Options)。

■ 空对象

        空对象是TypeScript的一种特殊值,也是一种特殊类型。空对象没有自定义属性,所以对自定义属性赋值就会报错。空对象只能使用继承的属性,即继承自原型对象Object.prototype的属性。

const obj = {};
obj.prop = 123; // 报错。变量obj的值是一个空对象,然后对obj.prop赋值就会报错
// TypeScript会推断变量obj的类型为空对象,实际执行的是下面的代码
const obj:{} = {};
obj.toString() // 正确。toString()是一个继承自原型对象的方法,TS允许在空对象上使用

        TypeScript 不允许动态添加属性,所以对象不能分步生成,必须生成时一次性声明所有属性。如果确实需要分步声明,一个比较好的方法是,使用扩展运算符(...)合成一个新对象

const pt0 = {};
const pt1 = { x: 3 };
const pt2 = { y: 4 };
const pt = {
  ...pt0, ...pt1, ...pt2
}; // 对象pt是三个部分合成的,这样既可以分步声明,也符合TypeScript静态声明的要求

        空对象作为类型,其实是Object类型的简写形式。又因为Object可以接受各种类型的值,所以它不会有严格字面量检查,赋值时总是允许多余的属性,只是不能读取这些属性。

let d:{};
// 等同于
let d:Object;
//各种类型的值(除了null和undefined)都可以赋值给空对象类型,跟Object类型的行为是一样的
d = {};
d = { x: 1 };
d = 'hello';
d = 2;

        如果想强制使用没有任何属性的对象,可以采用下面的写法。

interface WithoutProperties {
  [key: string]: never; //表示字符串属性名是不存在的
}
// 报错
const a:WithoutProperties = { prop: 1 }; //因此其他对象进行赋值时就会报错

10、interface

■ 简介

        interface是对象的模板,可以看作是一种类型约定,中文译为接口。使用了某个模板的对象,就拥有了指定的类型结构方括号运算符可以取出interface某个属性的类型

赋值时, 变量的形状必须和接口的形状保持一致。定义的变量比接口少一些属性、多一些属性都是不允许的。
interface Person {
  firstName: string;
  lastName: string;
  age: number;
} // 任何实现这个接口的对象,都必须部署这三个属性,并且必须符合规定的类型
const p:Person = { // 变量p的类型就是接口Person,所以必须符合Person指定的结构
  firstName: 'John',
  lastName: 'Smith',
  age: 25
}; // 实现该接口很简单,只要指定它作为对象的类型即可
type A = Person['age']; // number

        interface可以表示对象的各种语法,它的成员有5种形式:

  • 对象属性
    • 分别使用冒号指定每个属性的类型。
    • 属性之间使用分号或逗号分隔,最后一个属性结尾的分号或逗号可以省略。
    • 如果属性是可选的,就在属性名后面加一个问号
    • 如果属性是只读的,需要加上readonly修饰符
interface Point {
  x?: string;
  readonly y: string;
  [prop: string]: number;
}
  • 对象的属性索引
    • 属性索引共有string、number和symbol三种类型。
    • 一个接口中最多只能定义一个字符串索引。字符串索引会约束该类型中所有名字为字符串的属性。
    • 属性的数值索引,其实是指定数组的类型
    • 一个接口中最多只能定义一个数值索引。数值索引会约束所有名称为数值的属性。
    • 如果一个interface同时定义了字符串索引和数值索引,那么数值索引必须服从于字符串索引。因为在JavaScript中,数值属性名最终是自动转换成字符串属性名。
interface C {
  [prop: number]: string;
}
const obj:C = ['a', 'b', 'c']; // 属性名的类型是数值,所以可以用数组对变量obj赋值
interface A {
  [prop: string]: number;
  [prop: number]: string; // 报错。数值索引的属性值类型与字符串索引不一致,就会报错
}
interface B {
  [prop: string]: number;
  [prop: number]: number; // 正确。数值索引必须兼容字符串索引的类型声明
}
  • 对象方法
    • 对象的方法共有三种写法。属性名可以采用表达式。
    • 类型方法可以重载。interface里面的函数重载不需要给出实现。但是,由于对象内部定义方法时,无法使用函数重载的语法,所以需要额外在对象外部给出函数方法的实现
// 写法一
interface A {
  f(x: boolean): string;
}
// 写法二
interface B {
  f: (x: boolean) => string;
}
// 写法三
interface C {
  f: { (x: boolean): string };
}
// 属性名可以采用表达式,所以下面的写法也是可以的。
const f = 'f';
interface A {
  [f](x: boolean): string;
}
interface A {
  f(): number;
  f(x: boolean): boolean;
  f(x: string, y: string): string;
} // 函数重载,不需要给出实现,需要额外在对象外部给出函数方法的实现
  • 函数
    • interface 也可以用来声明独立的函数
interface Add {
  (x:number, y:number): number;
} // 声明了一个函数类型
const myAdd:Add = (x,y) => x + y;
  • 构造函数
    • interface 内部可以使用new关键字,表示构造函数
interface ErrorConstructor {
  new (message?: string): Error; // 内部有new命令,表示它是一个构造函数
}
  • 有时我们希望一个接口允许有任意的属性,可以使用如下方式。
interface Person {
    name: string;
    age?: number;
    [propName: string]: any; 
} // 使用 [propName: string] 定义了任意属性取 string 类型的值。
interface Person {
    name: string;
    age?: number;
    [propName: string]: string;
} // 一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集
interface Person {
    name: string;
    age?: number;
    [propName: string]: string | number;
} // 如果接口中有多个类型的属性,则可以在任意属性中使用联合类型

■ interface 的继承

interface 继承 interface

        interface可以使用extends关键字,继承其他interface。extends关键字会从继承的接口里面拷贝属性类型。这样就不必书写重复的属性。

        interface 允许多重继承。多重接口继承,实际上相当于多个父接口的合并。多重继承时,如果多个父接口存在同名属性,那么这些同名属性不能有类型冲突,否则会报错。

interface Style {
  color: string;
}
interface Shape {
  name: string;
}
interface Circle extends Style, Shape {
  radius: number;
} // Circle同时继承了Style和Shape,所以拥有三个属性color、name和radius

        如果子接口与父接口存在同名属性,那么子接口的属性会覆盖父接口的属性。子接口与父接口的同名属性必须是类型兼容的,不能有冲突,否则会报错。

interface Foo {
  id: string;
}
interface Bar {
  id: number;
}
// 报错
interface Baz extends Foo, Bar {
  type: string;
} // Baz同时继承了Foo和Bar,但是后两者的同名属性id有类型冲突

interface 继承 type

        interface可以继承type命令定义的对象类型。如果type命令定义的类型不是对象,interface就无法继承。

type Country = {
  name: string;
  capital: string;
}
interface CountryWithPop extends Country {
  population: number;
} // CountryWithPop继承了type命令定义的Country对象,并且新增了一个population属性

interface 继承 class

        interface还可以继承class,即继承该类的所有成员。某些类拥有私有成员和保护成员,interface可以继承这样的类,但无法用于对象,意义不大。

class A { // A有私有成员和保护成员
  private x: string = '';
  protected y: string = '';
}
interface B extends A { // B继承了A,但无法用于对象,因为对象不能实现这些成员
  z: number
}
// 报错
const b:B = { /* ... */ }
// 报错
class C implements B { //致B只能用于其他class
  // ...
} // 这时其他class与A之间不构成父类和子类的关系,使得x与y无法部署

■ 接口合并

        多个同名接口会合并成一个接口。JavaScript开发者常常对全局对象或者外部库,添加自己的属性和方法。只要使用interface给出这些自定义属性和方法的类型,就能自动跟原始的interface合并,使得扩展外部类型非常方便。

        同名接口合并时,同一个属性如果有多个类型声明,彼此不能有类型冲突

        同名接口合并时,如果同名方法有不同的类型声明,那么会发生函数重载。而且,后面的定义比前面的定义具有更高的优先级。但是,如果有一个参数是字面量类型,字面量类型的优先级会排到最前面

        如果两个interface组成的联合类型存在同名属性,那么该属性的类型也是联合类型

接口的合并:接口中的属性在合并时会简单的合并到一个接口中,并的属性的类型必须是唯一的。

■ interface 与 type 的异同

        interface命令与type命令作用类似,都可以表示对象类型。很多对象类型既可用interface表示,也可用type表示。而且,两者往往可以换用,几乎所有的 interface 命令都可以改写为 type 命令

        它们的相似之处,表现在都能为对象类型起名

type Country = {
  name: string;
  capital: string;
}
interface Country {
  name: string;
  capital: string;
}
class命令也有类似作用,通过定义一个类,同时定义一个对象类型。但是,它会创造一个值,编译后依然存在。如果只是单纯想要一个类型,应该使用type或interface。

interface 与 type 的区别有下面几点:

  • type能够表示非对象类型,而interface只能表示对象类型(包括数组、函数等)。
  • interface可以继承其他类型,type不支持继承
    • 继承的主要作用是添加属性,type定义的对象类型如果想要添加属性,只能使用&运算符,重新定义一个类型。(&运算符表示同时具备两个类型的特征,可以起到两个对象类型合并的作用。)
    • interface添加属性,采用的是继承的写法。继承时,type 和 interface 是可以换用的。interface 可以继承 type。type 也可以继承 interface。
  • 同名interface会自动合并,同名type则会报错。即TS不允许使用type多次定义同一个类型。
  • interface不能包含属性映射(mapping),type可以
  • this关键字只能用于interface。
  • type可以扩展原始数据类型,interface 不行
  • interface无法表达某些复杂类型(比如交叉类型和联合类型),但是type可以
type Foo = { x: number; };
interface Bar extends Foo { // interface 可以继承 type
  y: number;
}
interface Foo {
  x: number;
}
type Bar = Foo & { y: number; }; // type 也可以继承 interface
如果有复杂的类型运算,那么没有其他选择只能使用type;一般情况下,interface灵活性比较高,便于扩充类型或自动合并,建议优先使用。

11、类

        类是面向对象编程的基本构件,封装了属性和方法,TypeScript 给予了全面支持。

这里对类相关的概念做一个简单的介绍。
类(Class):定义了一件事物的抽象特点, 包含它的属性和方法
对象(Object):类的实例,通过 new 生成
面向对象(OOP)的三大特性: 封装、继承、多态
封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能 通过对外提供的接口来访问该对象,同时也保证了外界 无法任意更改对象内部的数据
继承(Inheritance):子类 继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性
多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以 有不同的响应。比如 Cat 和 Dog 都继承自 Animal,但是分别实现了自己的 eat 方法。此时针对某一个实例,我们无需了解它是 Cat 还是 Dog,就可以直接调用 eat 方法,程序会自动判断出来应该如何执行 eat
存取器(getter & setter):用以 改变属性的读取和赋值行为
修饰符(Modifiers):修饰符是一些关键字, 用于限定成员或类型的性质。比如 public 表示公有属性或方法
抽象类(Abstract Class):抽象类是供其他类继承的基类,抽象类 不允许被实例化抽象类中的抽象方法必须在子类中被实现
接口(Interfaces):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(implements)。 一个类只能继承自另一个类,但是可以实现多个接口

■ 简介

属性的类型

        类的属性可以在顶层声明,也可以在构造方法内部声明。对于顶层声明的属性,可以在声明时同时给出类型。

        如果不给出类型,TypeScript 会认为是any。如果声明时给出初值,可以不写类型,TypeScript 会自行推断属性的类型。

        TypeScript 有一个配置项strictPropertyInitialization,只要打开(默认是打开的),就会检查属性是否设置了初值,如果没有就报错。

        如果类的顶层属性不赋值,就会报错。如果不希望出现报错,可以使用非空断言。就是说,在属性名后面添加感叹号,表示这两个属性肯定不会为空,TypeScript就不报错了。

class Point {
  x!: number;
  y!: number;
}
// 打开 strictPropertyInitialization
class Point {
  x: number; // 报错
  y: number; // 报错
}

readonly 修饰符

        属性名前面加上readonly修饰符,就表示该属性是只读的。实例对象不能修改这个属性

        readonly 属性的初始值,可以写在顶层属性,也可以写在构造方法里面

        构造方法内部设置只读属性的初值、修改只读属性的值,都是可以的。或者说,如果两个地方都设置了只读属性的值,以构造方法为准。在其他方法修改只读属性都会报错。

        如果 readonly 和其他访问修饰符同时存在的话,需要写在其后面

修饰符和readonly还可以使用在构造函数参数中,等同于类中定义该属性同时给该属性赋值,使代码更简洁。readonly只允许出现在属性声明或索引签名或构造函数中。

方法的类型

        类的方法就是普通函数,类型声明方式与函数一致。

        类的方法跟普通函数一样,可以使用参数默认值,以及函数重载

        构造方法可以接受一个参数,也可以接受两个参数,采用函数重载进行类型声明。另外,构造方法不能声明返回值类型,否则报错,因为它总是返回实例对象

存取器方法

        存取器(accessor)是特殊的类方法,包括取值器(getter)和存值器(setter)两种方法。它们用于读写某个属性,取值器用来读取属性,存值器用来写入属性

class C {
  _name = '';
  get name() { // 取值器,其中get是关键词,name是属性名
    return this._name;
  } // 外部读取name属性时,实例对象会自动调用这个方法,该方法的返回值就是name属性的值
  set name(value) { // 存值器,其中set是关键词,name是属性名
    this._name = value;
  } // 外部写入name属性时,实例对象会自动调用这个方法,并将所赋的值作为函数参数传入
} 

TypeScript 对存取器有以下规则。

  • 如果某个属性只有get方法,没有set方法,那么该属性自动成为只读属性。
  • TS 5.1版之前,set方法的参数类型,必须兼容get方法的返回值类型,否则报错。TS5.1 版做出了改变,现在两者可以不兼容。
  • get方法与set方法的可访问性必须一致,要么都为公开方法,要么都为私有方法。

属性索引

        类允许定义属性索引。

class MyClass {
  [s:string]: boolean |
    ((s:string) => boolean); //所有属性名类型为字符串的属性,属性值要么是布尔值,要么是返回布尔值的函数
  get(s:string) {
    return this[s] as boolean;
  }
}

        注意,由于类的方法是一种特殊属性(属性值为函数的属性),所以属性索引的类型定义也涵盖了方法。如果一个对象同时定义了属性索引和方法,那么前者必须包含后者的类型。

class MyClass {
  [s:string]: boolean | (() => boolean);
  f() {
    return true;
  }
}

        属性存取器视同属性。

class MyClass {
  [s:string]: boolean; // 属性索引虽然没有涉及方法类型,但是不会报错
  get isInstance() { // 读取器虽然是一个函数方法,但是视同属性
    return true;
  }
}

■ 类的 interface 接口

        有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口,用 implements 关键字来实现。

implements 关键字

        interface 接口或 type 别名,可以用对象的形式,为 class 指定一组检查条件。然后,类使用 implements 关键字,表示当前类满足这些外部类型条件的限制

        interface只是指定检查条件,如果不满足这些条件就会报错。它并不能代替class自身的类型声明。比如,类B实现了接口A,但A并不能代替B的类型声明,B类依然需要声明参数的类型,需要声明可选属性。

        类可以定义接口没有声明的方法和属性。表示除了满足接口给出的条件,类还有额外的条件。

        implements关键字后面,不仅可以是接口,也可以是另一个类。这时,后面的类将被当作接口。在接口继承类的时候,也只会继承它的实例属性和实例方法。

        注意,interface描述的是类的对外接口,也就是实例的公开属性和公开方法,不能定义私有的属性和方法。这是因为TypeScript设计者认为,私有属性是类的内部实现,接口作为模板,不应该涉及类的内部代码写法。

实现多个接口

        一个类可以实现多个接口(其实是接受多重限制),每个接口之间使用逗号分隔。但是,同时实现多个接口并不是一个好的写法,容易使得代码难以管理,可以使用两种方法替代。第一种方法是类的继承。第二种方法是接口的继承。在 TypeScript 中,接口与接口之间可以是继承关系。

class Car implements MotorVehicle {
}
class SecretCar extends Car implements Flyable, Swimmable { // 类的继承
}
interface A {
  a:number;
}
interface B extends A { // 接口的继承
  b:number;
}

        注意,发生多重实现时(即一个接口同时实现多个接口),不同接口不能有互相冲突的属性。

类与接口的合并

        TypeScript不允许两个同名的类,但是如果一个类和一个接口同名,那么接口会被合并进类。注意,合并进类的非空属性,如果在赋值之前读取,会返回undefined

类的合并:类的合并与接口的合并规则一致。

■ Class 类型

实例类型

        TypeScript的类本身就是一种类型,但是它代表该类的实例类型,而不是class的自身类型。

        对于引用实例对象的变量来说,既可以声明类型为 Class,也可以声明类型为 Interface,因为两者都代表实例对象的类型。

interface MotorVehicle {
}
class Car implements MotorVehicle {
}
// 写法一
const c1:Car = new Car();
// 写法二
const c2:MotorVehicle = new Car();

        作为类型使用时,类名只能表示实例的类型,不能表示类的自身类型。

        由于类名作为类型使用,实际上代表一个对象,因此可以把类看作为对象类型起名。事实上,TypeScript有三种方法可以为对象类型起名:type、interface 和 class。

类的自身类型

        类的自身类型就是一个构造函数,可以单独定义一个接口来表示要获得一个类的自身类型,一个简便的方法就是使用 typeof 运算符

        类只是构造函数的一种语法糖,本质上是构造函数的另一种写法。所以,类的自身类型可以写成构造函数的形式。构造函数也可以写成对象形式

        可以把构造函数提取出来,单独定义一个接口,这样可以大大提高代码的通用性。

interface PointConstructor {
  new(x:number, y:number):Point;
}
function createPoint(
  PointClass: PointConstructor,
  x: number,
  y: number
):Point {
  return new PointClass(x, y);
}

结构类型原则

        Class也遵循结构类型原则。一个对象只要满足 Class 的实例结构,就跟该 Class 属于同一个类型

        如果两个类的实例结构相同,那么这两个类就是兼容的,可以用在对方的使用场合。

        只要 A 类具有 B 类的结构,哪怕还有额外的属性和方法,TypeScript也认为 A 兼容 B 的类型

        不仅是类,如果某个对象跟某个 class 的实例结构相同,TypeScript也认为两者的类型相同。由于这种情况,运算符instanceof不适用于判断某个对象是否跟某个 class 属于同一类型

        空类不包含任何成员,任何其他类都可以看作与空类结构相同。因此,凡是类型为空类的地方,所有类(包括对象)都可以使用。

        注意,确定两个类的兼容关系时,只检查实例成员,不考虑静态成员和构造方法。

        如果类中存在私有成员(private)或保护成员(protected),那么确定兼容关系时,TypeScript 要求私有成员和保护成员来自同一个类,这意味着两个类需要存在继承关系

■ 类的继承

        类(这里又称子类)可以使用extends关键字继承另一个类(这里又称基类)的所有属性和方法。一般来讲,一个类只能继承自另一个类

        根据结构类型原则,子类也可以用于类型为基类的场合。子类可以覆盖基类的同名方法。

        使用super关键字指代基类是常见做法。

        子类的同名方法不能与基类的类型定义相冲突

        如果基类包括保护成员(protected修饰符),子类可以将该成员的可访问性设置为公开(public修饰符),也可以保持保护成员不变,但是不能改用私有成员(private修饰符)。

        extends关键字后面不一定是类名,可以是一个表达式,只要它的类型是构造函数就可以了

        对于那些只设置了类型、没有初值的顶层属性,有一个细节需要注意。没有设置初值,代码在不同的编译设置下编译结果不一样。解决方法就是使用declare命令,去声明顶层成员的类型,告诉 TS这些成员的赋值由基类实现。

■ 可访问性修饰符

        类的内部成员的外部可访问性,由三个可访问性修饰符控制:public、private和protected。这三个修饰符的位置,都写在属性或方法的最前面

public

        public修饰符表示这是公开成员,外部可以自由访问。是默认修饰符,如果省略不写,实际上就带有该修饰符。因此,类的属性和方法默认都是外部可访问的。正常情况下,除非为了醒目和代码可读性,public都是省略不写的。

  • public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的

private

        private修饰符表示私有成员,只能用在当前类的内部类的实例和子类都不能使用该成员。如果在类的内部当前类的实例可以获取私有成员。

  • private 修饰的属性或方法是私有的,不能在声明它的类的外部访问
严格地说,private定义的私有成员,并不是真正意义的私有成员。一方面,编译成JavaScript后,private关键字就被剥离了,这时外部访问该成员就不会报错。另一方面,由于前一个原因,TypeScript对于访问private成员没有严格禁止,使用方括号写法([])或者in运算符,实例对象就能访问该成员。
建议不使用private,改用 ES2022 的写法(属性名前加#),获得真正意义的私有成员。

        构造方法也可以是私有的,这就直接防止了使用new命令生成实例对象(实现了单例模式),只能在类的内部创建实例对象。当构造函数修饰为 private 时,该类不允许被继承或者实例化,当构造函数修饰为 protected 时,该类只允许被继承

protected

        protected修饰符表示该成员是保护成员,只能在类的内部使用该成员,实例无法使用该成员,但是子类内部可以使用。子类不仅可以拿到父类的保护成员,还可以定义同名成员。在类的外部,实例对象不能读取保护成员,但是在类的内部可以。

  • protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的

实例属性的简写形式

        实际开发中,很多实例属性的值,是通过构造方法传入的。

class Point {
  x:number;
  y:number;
  constructor(x:number, y:number) { // 属性x和y的值是通过构造方法的参数传入的
    this.x = x;
    this.y = y;
  }
}

        这样的写法等于对同一个属性要声明两次类型,一次在类的头部,另一次在构造方法的参数里面。这有些累赘,TypeScript 就提供了一种简写形式。

class Point {
  constructor(
    public x:number, //这里的public不能省略
    public y:number
  ) {}
}
const p = new Point(10, 10);
p.x // 10
p.y // 10
构造方法的参数x前面有public修饰符,这时 TypeScript 就会自动声明一个公开属性x,不必在构造方法里面写任何代码,同时还会设置x的值为构造方法的参数值。

        除了public修饰符,构造方法的参数名只要有private、protected、readonly修饰符,都会自动声明对应修饰符的实例属性。readonly还可以与其他三个可访问性修饰符,一起使用。

■ 静态成员

        类的内部可以使用static关键字,定义静态成员。静态成员是只能通过类本身使用的成员,不能通过实例对象使用。static关键字前面可以使用 public、private、protected 修饰符。静态私有属性也可以用ES6语法的 #前缀 表示。public和protected的静态成员可以被继承

■ 泛型类

        类也可以写成泛型,使用类型参数。注意,静态成员不能使用泛型的类型参数

class Box<Type> { // 类Box有类型参数Type,因此属于泛型类。
  contents: Type;
  constructor(value:Type) {
    this.contents = value;
  }
}
// 新建实例时,变量的类型声明需要带有类型参数的值,不过本例等号左边的Box<string>可以省略不写
const b:Box<string> = new Box('hello!');  // 因为可以从等号右边推断得到

■ 抽象类,抽象成员

        TypeScript允许在类的定义前面,加上关键字abstract,表示该类不能被实例化,只能当作其他类的模板。这种类就叫做抽象类。抽象类只能当作基类使用,用来在它的基础上定义子类

抽象类是不允许被实例化的。抽象类中的抽象方法必须被子类实现。即使是抽象方法,TypeScript 的编译结果中,仍然会存在这个类。

        抽象类的作用是,确保各种相关的子类都拥有跟基类相同的接口,可以看作是模板。其中的抽象成员都是必须由子类实现的成员,非抽象成员则表示基类已经实现的、由所有子类共享的成员。

        抽象类的子类也可以是抽象类,也就是说,抽象类可以继承其他抽象类。

        抽象类的内部可以有已经实现好的属性和方法,也可以有未实现的属性和方法。后者叫抽象成员,即属性名和方法名有abstract关键字,表示该方法需要子类实现。如果子类没有实现抽象成员,就会报错。

这里有几个注意点。

  • 抽象成员只能存在于抽象类,不能存在于普通类。
  • 抽象成员不能有具体实现的代码。也就是说,已经实现好的成员前面不能加abstract关键字。
  • 抽象成员前也不能有private修饰符,否则无法在子类中实现该成员。
  • 一个子类最多只能继承一个抽象类。

■ this 问题

        类的方法经常用到this关键字,它表示该方法当前所在的对象

        TypeScript 允许函数增加一个名为this的参数,放在参数列表的第一位,用来描述函数内部的this关键字的类型。编译时,TypeScript 一旦发现函数的第一个参数名为this,则会去除这个参数,即编译结果不会带有该参数。

        this参数的类型可以声明为各种对象

        在类的内部,this本身也可以当作类型使用,表示当前类的实例对象

        TypeScript提供了一个noImplicitThis编译选项。如果打开了这个设置项,如果this的值推断为any类型,就会报错。

        注意,this类型不允许应用于静态成员。静态成员拿不到实例对象。

        有些方法返回一个布尔值,表示当前的this是否属于某种类型。这时,这些方法的返回值类型可以写成this is Type的形式,其中用到了is运算符。

12、泛型

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

■ 简介

        反映出参数与返回值之间的类型关系。泛型的特点就是带有类型参数。参数要放在一对尖括号<>里面,可以将其理解为类型声明需要的变量,需要在调用时传入具体的参数类型。类型参数的名字,可以随便取,但是必须为合法的标识符。

        泛型本质上是一个类型函数,通过输入参数,获得结果,两者是一一对应关系。

function getFirst<T>(arr:T[]):T {
  return arr[0];
} // 函数getFirst()的参数类型是T[],返回值类型是T,就清楚地表示了两者之间的关系
getFirst<number>([1, 2, 3]) //函数调用时,需要提供类型参数。
getFirst([1, 2, 3]) //为了方便,函数调用时,往往省略不写类型参数的值,让TS自己推断
//有些复杂的使用场景,TypeScript 可能推断不出类型参数的值,这时就必须显式给出了。
comb<number|string>([1, 2], ['a', 'b']) // 正确
泛型可以理解成一段类型逻辑,需要类型参数来表达。有了类型参数以后,可以在输入类型与输出类型之间,建立一一对应关系。

■ 泛型的写法

        泛型主要用在四个场合:函数、接口、类和别名。

函数的泛型写法

        function关键字定义的泛型函数,类型参数放在尖括号中,写在函数名后面。

function id<T>(arg:T):T {
  return arg;
}

        那么对于变量形式定义的函数,泛型有下面两种写法。

// 写法一
let myId:<T>(arg:T) => T = id;
// 写法二
let myId:{ <T>(arg:T): T } = id;

接口的泛型写法

        使用接口的方式来定义一个函数需要符合的形状,也可以使用含有泛型的接口来定义函数的形状,可以把泛型参数提前到接口名上,此时在使用泛型接口的时候,需要定义泛型的类型

// 第一种写法,类型参数定义在整个接口,接口内部的所有属性和方法都可以使用该类型参数--------
interface Box<Type> {
  contents: Type;
}
let box:Box<string>; // 使用泛型接口时,需要给出类型参数的值(本例是string)
// 第二种写法,类型参数定义在某个方法之中,其他属性和方法不能使用该类型参数---------------
interface Fn {
  <Type>(arg:Type): Type; // Fn的类型参数Type的具体类型,需要函数id在使用时提供
}
function id<Type>(arg:Type): Type { 
  return arg;
}
let myId:Fn = id; // 所以,最后一行的赋值语句不需要给出Type的具体类型。

类的泛型写法

        与泛型接口类似,泛型也可以用于类的类型定义中。泛型类的类型参数写在类名后面。

class A<T> { // 类A有一个类型参数T
  value: T;
}
class B extends A<any> { // 使用时必须给出T的类型
}
// 泛型也可以用在类表达式
const Container = class<T> {
  constructor(private readonly data:T) {} 
}; // 新建实例时,需要同时给出类型参数T和类参数data的值
const a = new Container<boolean>(true);
const b = new Container<number>(0);

        JavaScript 的类本质上是一个构造函数,因此也可以把泛型类写成构造函数。

type MyClass<T> = new (...args: any[]) => T;
// 或者
interface MyClass<T> {
  new(...args: any[]): T;
}
// 用法实例
function createInstance<T>( // T是createInstance()的类型参数,调用时再指定具体类型
  AnyClass: MyClass<T>, // AnyClass是构造函数(也可以是一个类),它的类型是MyClass<T>
  ...args: any[]
):T {
  return new AnyClass(...args);
}

        注意,泛型类描述的是类的实例,不包括静态属性和静态方法,因为这两者定义在类的本身。因此,它们不能引用类型参数。类型参数只能用于实例属性和实例方法。

类型别名的泛型写法

        type 命令定义的类型别名,也可以使用泛型。

type Nullable<T> = T | undefined | null; //传入一个类型,得到它与undefined和null的联合类型

■ 类型参数的默认值

        可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用。

        类型参数可以设置默认值。使用时,如果没有给出类型参数的值,就会使用默认值。用在函数中时,TypeScript会从实际参数推断出T的值,从而覆盖掉默认值。类型参数的默认值,往往用在类中。

        类型参数有默认值,就表示它是可选参数。如果有多个类型参数,可选参数必须在必选参数之后

■ 数组的泛型表示

        Array<any> 允许数组的每一项都为任意类型。但是我们预期的是,数组中每一项都应该是输入的 value 的类型。这时候,泛型就派上用场了,我们在函数名后添加了 <T>,其中 T 用来指代任意输入的类型,在后面的输入 value: T 和输出 Array<T> 中即可使用了。接着在调用的时候,可以指定它具体的类型为 string。当然,也可以不手动指定,而让类型推论自动推算出来。

function createArray<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}
createArray<string>(3, 'x'); // ['x', 'x', 'x']

        事实上,在TS内部,数组类型的另一种写法number[]、string[],只是Array<number>、Array<string>的简写形式。

        在TypeScript内部,Array是一个泛型接口,还提供一个ReadonlyArray<T>接口,表示只读数组。

let arr:Array<number> = [1, 2, 3]; // 表示该数组的全部成员都是数值
function doStuff(
  values:ReadonlyArray<string> // 表示不能修改这个数组
) {
  values.push('hello!');  // 报错
}

■ 类型参数的约束条件

        TypeScript提供了一种语法,允许在类型参数上面写明约束条件,如果不满足条件,编译时就会报错。这样也可以有良好的语义,对类型参数进行说明。

/* TypeParameter表示类型参数,
extends是关键字,这是必须的,
ConstraintType表示类型参数要满足的条件 */
<TypeParameter extends ConstraintType> // 即类型参数应该是ConstraintType的子类型

        类型参数可以同时设置约束条件和默认值,前提是默认值必须满足约束条件。

        如果有多个类型参数,一个类型参数的约束条件可以引用其他参数。但是,约束条件不能引用类型参数自身。同理,多个类型参数也不能互相约束。

■ 使用注意点

  • 尽量少用泛型。泛型虽然灵活,但是会加大代码的复杂性,使其变得难读难写。
  • 类型参数越少越好。多一个类型参数,多一道替换步骤,加大复杂性。
  • 类型参数需要出现两次。只有当类型参数用到两次或两次以上,才是泛型的适用场合。
  • 泛型可以嵌套。类型参数可以是另一个泛型。

13、Enum 类型

■ 简介

        TypeScript 设计了 Enum 结构,用来将相关常量放在一个容器里面,方便使用

        使用时,调用 Enum 的某个成员,与调用对象属性的写法一样,可以使用点运算符,也可以使用方括号运算符

        Enum 结构本身也是一种类型。Enum 结构的特别之处在于,它既是一种类型,也是一个值。绝大多数 TypeScript 语法都是类型语法,编译后会全部去除,但是 Enum 结构是一个值,编译后会变成 JavaScript 对象,留在代码中。由于 Enum 结构编译后是一个对象,所以不能有与它同名的变量(包括对象、函数、类等)。很大程度上,Enum 结构可以被对象的as const断言替代。

        Enum结构较适合的场景是,成员的值不重要,名字更重要,从而增加代码的可读性和可维护性。

        枚举项有两种类型:常数项(constant member)和计算所得项(computed member)。

■ Enum 成员的值

        Enum成员默认不必赋值,系统会从零开始逐一递增,按照顺序为每个成员赋值,比如0、1、2…但是也可以为 Enum成员显式赋值。如果只设定第一个成员的值,后面成员的值就会从这个值开始递增

        成员的值可以是任意数值,但不能是大整数(Bigint)。成员的值甚至可以相同

        Enum成员的值也可以使用计算式

        Enum成员值都是只读的,不能重新赋值。为了让这一点更醒目,通常会在enum关键字前面加上const修饰,表示这是常量,不能再次赋值。加上const,编译为JavaScript代码后,编译产物里面就没有生成对应的对象,而是把所有 Enum 成员出现的场合,都替换成对应的常量。

        如果希望加上const关键词后,运行时还能访问 Enum 结构(即编译后依然将 Enum 转成对象),需要在编译时打开preserveConstEnums编译选项。

■ 同名 Enum 的合并

        多个同名的 Enum 结构会自动合并。同名 Enum 合并时,不能有同名成员,否则报错。所有定义必须同为 const 枚举或者非 const 枚举,不允许混合使用。同名 Enum 的合并,最大用处就是补充外部定义的 Enum 结构。

enum Foo {
  A,
}
enum Foo {
  B = 1,
}
enum Foo {
  C = 2,
}
// 等同于
enum Foo {
  A, // Enum 结构合并时,只允许其中一个的首成员省略初始值,否则报错。
  B = 1,
  C = 2
}

■ 常数 Enum

        常数枚举是使用 const enum 定义的枚举类型。常数枚举与普通枚举的区别是,它会在编译阶段被删除,并且不能包含计算成员

const enum Directions {
    Up,
    Down,
    Left,
    Right
}
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];

■ 字符串 Enum

        有些场合,开发者可能希望Enum成员值可以保存一些有用信息,所以TS才设计了字符串Enum。枚举(Enum)类型用于取值被限定在一定范围内的场景,比如一周只能有七天,颜色限定为红绿蓝等。

        Enum成员的值除了设为数值,还可以设为字符串。也就是说,Enum也可以用作一组相关字符串的集合。注意,字符串枚举的所有成员值,都必须显式设置。如果没有设置,成员值默认为数值,且位置必须在字符串成员之前Enum 成员可以是字符串和数值混合赋值。除了数值和字符串,Enum 成员不允许使用其他值(比如 Symbol 值)。

enum Enum {
  One = 'One',
  Two = 'Two',
  Three = 3,
  Four = 4,
}

        变量类型如果是字符串 Enum,就不能再赋值为字符串,这跟数值 Enum 不一样。由于这个原因,如果函数的参数类型是字符串 Enum,传参时就不能直接传入字符串,而要传入 Enum 成员。因此,字符串 Enum 作为一种类型,有限定函数参数的作用。

        字符串 Enum 可以使用联合类型代替。字符串 Enum 的成员值,不能使用表达式赋值

function move(
  where:'Up'|'Down'|'Left'|'Right' // 函数参数where属于联合类型,效果跟指定为字符串Enum一样
) {
  // ...
 }
enum MyEnum {
  A = 'one',
  B = ['T', 'w', 'o'].join('') // 报错。成员B的值是一个字符串表达式,导致报错
}

■ 外部枚举 Enum

        外部枚举(Ambient Enums)是使用 declare enum 定义的枚举类型。declare 定义的类型只会用于编译时的检查,编译结果中会被删除

declare enum Directions {
    Up,
    Down,
    Left,
    Right
}
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
外部枚举与声明语句一样,常出现在声明文件中。
同时使用 declare 和 const 也是可以的。

■ keyof 运算符

        keyof 运算符可以取出 Enum 结构的所有成员名,作 为联合类型返回

enum MyEnum {
  A = 'a',
  B = 'b'
}
// 'A'|'B'
type Foo = keyof typeof MyEnum; // 类型Foo等同于联合类型'A'|'B'
// 注意,这里的typeof是必需的,否则keyof MyEnum相当于keyof string
type Foo = keyof MyEnum; // number | typeof Symbol.iterator | "toString" | "charAt" | "charCodeAt" | ...
Enum 作为类型,本质上属于number或string的一种变体,而typeof MyEnum会将MyEnum当作一个值处理,从而先其转为对象类型,就可以再用keyof运算符返回该对象的所有属性名。

        如果要返回 Enum 所有的成员值,可以使用in运算符。

enum MyEnum {
  A = 'a',
  B = 'b'
}
// 采用属性索引可以取出MyEnum的所有成员值
type Foo = { [key in MyEnum]: any }; // { a: any, b: any }

■ 反向映射

        数值Enum存在反向映射,即可以通过成员值获得成员名。注意,这种情况只发生在数值Enum,对于字符串Enum,不存在反向映射。这是因为字符串Enum编译后只有一组赋值。

14、类型断言

■ 简介

        类型断言可以用来手动指定一个值的类型允许开发者在代码中断言某个值的类型,告诉编译器此处的值是什么类型。TypeScript 一旦发现存在类型断言,就不再对该值进行类型推断,而是直接采用断言给出的类型。类型断言并不是真的改变一个值的类型,而是提示编译器,应该如何处理这个值。

这种做法的实质是,允许开发者在某个位置绕过编译器的类型推断,让本来通不过类型检查的代码能够通过,避免编译器报错。使用类型断言时一定要格外小心,减少不必要的运行时错误。
// 语法一:<类型>值
<Type>value
// 语法二:值 as 类型   推荐使用语法二
value as Type

        类型断言的一大用处是,指定 unknown 类型的变量的具体类型。unknown 类型的变量value不能直接赋值给其他类型的变量,但是可以将它断言为其他类型,这样就可以赋值给别的变量了。

类型断言的常见用途有以下几种:
① 将一个联合类型断言为其中一个类型(联合类型可以被断言为其中一个类型)
② 将一个父类断言为更加具体的子类(父类可以被断言为子类)
③ 将任何一个类型断言为 any(任何类型都可以被断言为 any)
④ 将 any 断言为一个具体的类型(any 可以被断言为任何类型)
要使得 A 能够被断言为 B,只需要 A 兼容 B 或 B 兼容 A 即可
其实前四种情况都是最后一个的特例。
  • 类型断言 vs 类型转换:类型断言只会影响TS编译时的类型,语句在编译结果中会被删除。类型断言不是类型转换,它不会真的影响到变量的类型。若要进行类型转换,需要直接调用类型转换的方法。
  • 类型断言 vs 类型声明:类型声明比类型断言更加严格,最好优先使用类型声明。核心区别如下:
interface Animal {
    name: string;
}
interface Cat {
    name: string;
    run(): void;
}
const animal: Animal = {
    name: 'tom'
};
let tom = animal as Cat; // 正常。Animal兼容Cat,故可以将animal断言为Cat赋值给tom
let tom: Cat = animal; // 报错。不允许将animal赋值为Cat类型的tom
  • animal 断言为 Cat,只需要满足 Animal 兼容 Cat 或 Cat 兼容 Animal 即可。
  • animal 赋值给 tom,需要满足 Cat 兼容 Animal 才行,但是 Cat 并不兼容 Animal。
  • 类型断言 vs 泛型:类型断言主要用于处理编译器无法推断或确定类型的情况,而泛型则用于创建灵活且可重用的代码结构。

■ 类型断言的条件

        类型断言并不意味着,可以把某个值断言为任意类型。类型断言要求实际的类型与断言的类型兼容,实际类型可以断言为一个更加宽泛的类型(父类型),也可以断言为一个更加精确的类型(子类型),但不能断言为一个完全无关的类型

但是,如果真的要断言成一个完全无关的类型,也是可以做到的。那就是连续进行两次类型断言,先断言成 unknown 类型或 any 类型,然后再断言为目标类型。因为any类型和unknown类型是所有其他类型的父类型,所以可以作为两种完全无关的类型的中介。
// 或者写成 <T><unknown>expr
expr as unknown as T // 第一次断言为unknown类型,第二次断言为T类型。这样expr就可以断言成任意类型T而不报错

■ as const 断言

        as const会将字面量的类型断言为不可变类型,缩小成 TypeScript 允许的最小类型。

        如果没有声明变量类型,let命令声明的变量,会被类型推断为TypeScript内置的基本类型之一;const命令声明的变量,则被推断为值类型常量。有些时候,let变量会出现一些意想不到的报错,变更成const变量就能消除报错。

let s1 = 'JavaScript'; // 类型推断为基本类型 string
const s2 = 'JavaScript'; // 类型推断为字符串 “JavaScript”
// 后者是前者的子类型,相当于 const 命令有更强的限定作用,可以缩小变量的类型范围

        TypeScript提供了一种特殊的类型断言as const,用于告诉编译器,推断类型时,可以将这个值推断为常量,即把 let 变量断言为 const 变量,从而把内置的基本类型变更为值类型。使用了as const断言以后,let 变量就不能再改变值了。

        as const断言只能用于字面量,不能用于变量。as const也不能用于表达式。可写成前置形式。

// 后置形式
expr as const
// 前置形式
<const>expr

        as const断言可用于整个对象,也可用于对象的单个属性,这时它的类型缩小效果是不一样的。

const v1 = {
  x: 1,
  y: 2,
}; // 类型是 { x: number; y: number; }
const v2 = {
  x: 1 as const,
  y: 2,
}; // 类型是 { x: 1; y: number; }
const v3 = {
  x: 1,
  y: 2,
} as const; // 类型是 { readonly x: 1; readonly y: 2; }

        由于as const会将数组变成只读元组,所以很适合用于函数的rest参数。事实上,对于固定参数个数的函数,如果传入的参数包含扩展运算符,那么扩展运算符只能用于元组。只有当函数定义使用了 rest 参数,扩展运算符才能用于数组。

        Enum 成员也可以使用as const断言。

■ 非空断言

        对于那些可能为空的变量(即可能等于undefined或null),TypeScript 提供了非空断言,保证这些变量不会为空,写法是在变量名后面加上感叹号!

        非空断言在实际编程中很有用,有时可以省去一些额外的判断。不过,非空断言会造成安全隐患,只有在确定一个表达式的值不为空时才能使用。比较保险的做法还是手动检查一下是否为空。

        非空断言还可以用于赋值断言。TypeScript 有一个编译设置,要求类的属性必须初始化(即有初始值),如果不对属性赋值就会报错,可以使用非空断言表示这两个属性肯定会有值,这样就不会报错了。

非空断言只有在打开编译选项 strictNullChecks时才有意义。如果不打开这个选项,编译器就不会检查某个变量是否可能为undefined或null。
function f(x?:number|null) { // 参数x的类型是number|null,即可能为空
  validateNumber(x); // 自定义函数,确保 x 是数值
  console.log(x!.toFixed()); // 前置检验,变量x肯定不会为空
}

■ 断言函数

        断言函数是一种特殊函数,用于保证函数参数符合某种类型。如果函数参数达不到要求,就会抛出错误,中断程序执行;如果达到要求,就不进行任何操作,让代码按照正常流程运行。

/* asserts和is都是关键词,value是函数的参数名,string是函数参数的预期类型
   意思是,该函数用来断言参数value的类型是string,如果达不到要求,程序就会在这里中断*/
function isString(value:unknown):asserts value is string {
  if (typeof value !== 'string')
    throw new Error('Not a string');
} // 只要执行了该函数,对应的变量都为断言的类型
注意,函数返回值的断言写法,只是用来更清晰地表达函数意图,真正的检查是需要开发者自己部署的。而且,如果内部的检查与断言不一致,TypeScript 也不会报错。
断言函数的asserts语句等同于void类型,所以如果返回除了undefined和null以外的值,会报错。

        如果要断言参数非空,可以使用工具类型NonNullable<T>。

        断言函数与类型保护函数是两种不同的函数。它们的区别是,断言函数不返回值,而类型保护函数总是返回一个布尔值。

        如果要断言某个参数保证为真(即不等于false、undefined和null),TypeScript 提供了断言函数的一种简写形式。同样的,参数为真的实际检查需要开发者自己实现。

function assert(x:unknown):asserts x { // asserts x省略了谓语和宾语,表示参数x保证为真true
  // ...
}

15、模块

■ 简介

        任何包含 import 或 export 语句的文件,就是一个模块。相应地,如果文件不包含 export 语句,就是一个全局的脚本文件。

        模块本身就是一个作用域,不属于全局作用域。模块内部的变量、函数、类只在内部可见,对于模块外部是不可见的。暴露给外部的接口,必须用 export 命令声明;如果其他文件要使用模块的接口,必须用 import 命令来输入。

        如果一个文件不包含export语句,但是希望把它当作一个模块(即内部变量对外不可见),可以在脚本头部添加一行语句。

export {}; //这行语句不产生任何实际作用,但会让当前文件被当作模块处理,所有它的代码都变成了内部代码

        TypeScript模块除了支持所有ES模块的语法,特别之处在于允许输出和输入类型。

        TypeScript 允许加载模块时,省略模块文件的后缀名,它会自动定位。

        编译时,可以两个脚本同时编译。

■ import type 语句

        import 在一条语句中,可以同时输入类型和正常接口。这样很不利于区分类型和正常接口,容易造成混淆。为了解决这个问题,TypeScript 引入了两个解决方法。

// 第一个方法是在 import 语句输入的类型前面加上type关键字。
import { type A, a } from './a';
// 第二个方法是使用 import type 语句,这个语句只用来输入类型,不用来输入正常接口。
import type { A } from './a'; // 输入类型A是正确的,可以把A当作类型使用
let b:A = 'hello'; // 正确
import type { a } from './a'; // 输入正常接口a,并把a当作一个值使用,就会报错
let b = a; // 报错

        import type 语句也可以输入默认类型。

import type DefaultType from 'moduleA';
import type * as TypeNS from 'moduleA'; // 在一个名称空间下,输入所有类型的写法如下

        同样的,export 语句也有两种方法,表示输出的是类型。

type A = 'a';
type B = 'b';
// 方法一 使用type关键字作为前缀
export {type A, type B};
// 方法二 使用 export type 语句,表示整行输出的都是类型
export type {A, B};

■ importsNotUsedAsValues 编译设置

        TypeScript 提供了importsNotUsedAsValues编译设置项,有三个可能的值。

  • remove:这是默认值,自动删除输入类型的 import 语句。
  • preserve:保留输入类型的 import 语句。
  • error:保留输入类型的 import 语句(与preserve相同),但必须写成import type形式,否则报错。

■ CommonJS 模块

        CommonJS 是 Node.js 的专用模块格式,与 ES 模块格式不兼容。

import = 语句

        使用import =语句输入 CommonJS 模块。

import * as fs from 'fs'; // 允许用import * as [接口名] from "模块文件"输入CommonJS模块
// 等同于
import fs = require('fs'); // 用import =语句和require()命令输入了一个CommonJS模块

export = 语句

        使用export =语句输出 CommonJS 模块的对象,等同于 CommonJS 的module.exports对象。

        export =语句输出的对象,只能使用import =语句加载。

let obj = { foo: 123 };
export = obj;
import obj = require('./a');
console.log(obj.foo); // 123

■ 模块定位

        模块定位指的是一种算法,用来确定 import 语句和 export 语句里面的模块文件位置。

// 相对模块
import { TypeA } from './a';
// 非相对模块
import * as $ from "jquery";

        编译参数moduleResolution,用来指定具体使用哪一种定位算法。常用的算法有两种:Classic和Node。如果没有指定moduleResolution,默认值与编译参数module有关。module设为commonjs时,moduleResolution的默认值为Node,即采用 Node.js 的模块定位算法。其他情况下(module设为 es2015、 esnext、amd, system, umd 等等),就采用Classic定位算法。

相对模块,非相对模块

        相对模块指的是路径以/、./、../开头的模块。相对模块的定位,是根据当前脚本的位置进行计算的,一般用于保存在当前项目目录结构中的模块脚本。

        非相对模块指的是不带有路径信息的模块。非相对模块的定位,是由baseUrl属性或模块映射而确定的,通常用于加载外部模块。

Classic 方法

        Classic 方法以当前脚本的路径作为“基准路径”,计算相对模块的位置。

        至于非相对模块,也是以当前脚本的路径作为起点,一层层查找上级目录。

Node 方法

        Node 方法就是模拟 Node.js 的模块加载方法,也就是require()的实现方法。

        相对模块依然是以当前脚本的路径作为“基准路径”

        非相对模块则是以当前脚本的路径作为起点,逐级向上层目录查找是否存在子目录node_modules。

路径映射

        TypeScript 允许开发者在tsconfig.json文件里面,手动指定脚本模块的路径。

  • baseUrl:可以手动指定脚本模块的基准目录
  • paths:指定非相对路径的模块与实际脚本的映射。
  • rootDirs:指定模块定位时必须查找的其他目录。

tsc 的--traceResolution参数

        能够在编译时在命令行显示模块定位的每一步

tsc --traceResolution // 会输出模块定位的判断过程

tsc 的--noResolve参数

        表示模块定位时,只考虑在命令行传入的模块

import * as A from "moduleA";
import * as B from "moduleB";
# 可以定位到moduleA.ts,因为它从命令行传入了,无法定位到moduleB,因为它没有传入,因此会报错
tsc app.ts moduleA.ts --noResolve 

16、namespace

        namespace 是一种将相关代码组织在一起的方式,中文译为“命名空间”。自从有了 ES 模块,官方已经不推荐使用 namespace 了。

■ 基本用法

        namespace 用来建立一个容器,内部的所有变量和函数,都必须在这个容器里面使用

        如果要在命名空间以外使用内部成员,就必须为该成员加上export前缀,表示对外输出该成员。

        namespace 内部还可以使用import命令输入外部成员,相当于为外部成员起别名。当外部成员的名字比较长时,别名能够简化代码。import命令也可以在 namespace 外部指定别名。

        使用嵌套的命名空间,必须从最外层开始引用。

        namespace不仅可以包含实义代码,还可以包括类型代码

        namespace 与模块的作用是一致的,都是把相关代码组织在一起,对外输出接口。区别是一个文件只能有一个模块,但可以有多个 namespace。由于模块可以取代 namespace,而且是 JavaScript 的标准语法,还不需要编译转换,所以建议总是使用模块替代 namespace。

        如果 namespace 代码放在一个单独的文件里,那么引入这个文件需要使用三斜杠的语法。

/// <reference path = "SomeFileName.ts" />

■ namespace 的输出

        namespace 本身也可以使用export命令输出,供其他文件使用。

// shapes.ts
export namespace Shapes { // 输出了一个命名空间Shapes
  export class Triangle {
    // ...
  }
  export class Square {
    // ...
  }
}

        其他脚本文件使用import命令加载这个命名空间。

// 写法一
import { Shapes } from './shapes';
let t = new Shapes.Triangle();
// 写法二
import * as shapes from "./shapes";
let t = new shapes.Shapes.Triangle();

        不过,更好的方法还是建议使用模块,采用模块的输出和输入

// shapes.ts
export class Triangle {
  /* ... */
}
export class Square {
  /* ... */
}
// shapeConsumer.ts
import * as shapes from "./shapes";
let t = new shapes.Triangle();

■ namespace 的合并

        多个同名的namespace会自动合并,这一点跟interface一样。如果同名的命名空间分布在不同的文件中,TypeScript最终会将它们合并在一起。这样就比较方便扩展别人的代码。

        合并命名空间时,命名空间中非export的成员不会被合并,但它们只能在各自的命名空间中使用

        命名空间还可以跟同名函数合并,但是要求同名函数必须在命名空间之前声明。这样做是为了确保先创建出一个函数对象,然后同名的命名空间就相当于给这个函数对象添加额外的属性。

        命名空间也能与同名 class 合并,同样要求class 必须在命名空间之前声明,原因同上。

        命名空间还能与同名 Enum 合并。注意,Enum成员与命名空间导出成员不允许同名。

17、装饰器

■ 简介

        装饰器是一种语法结构,用来在定义时修改类的行为

        在语法上,装饰器有如下几个特征。

  • 第一个字符(或者说前缀)是@,后面是一个表达式。
  • @后面的表达式,必须是一个函数(或者执行后可以得到一个函数)
  • 这个函数接受所修饰对象的一些相关值作为参数。
  • 这个函数要么不返回值,要么返回一个新对象取代所修饰的目标对象。
// 装饰器有多种形式,基本上只要在@符号后面添加表达式都是可以的
@myFunc
@myFuncFactory(arg1, arg2)
@libraryModule.prop
@someObj.method(123)
@(wrap(dict['prop']))

        相比使用子类改变父类,装饰器更加简洁优雅,缺点是不那么直观,功能也受到一些限制。所以,装饰器一般只用来为类添加某种特定行为

@frozen class Foo { // 一个用在类本身
  @configurable(false) //三个用在类的方法
  @enumerable(true)
  method() {}
  @throttle(500)
  expensiveMethod() {}
}

■ 装饰器的版本

        标准语法可以直接使用,传统语法需要打开--experimentalDecorators编译参数。

tsc --target ES5 --experimentalDecorators

■ 装饰器的结构

        Decorator是装饰器的类型定义。它是一个函数,使用时会接收到value和context两个参数

  • value:所装饰的对象。
  • context:上下文对象,TS提供一个原生接口ClassMethodDecoratorContext,描述这个对象。
    • context对象的属性,根据所装饰对象的不同而不同,其中只有两个属性(kind和name)是必有的,其他都是可选的。
      • kind:字符串,表示所装饰对象的类型。
      • name:字符串或者 Symbol 值,所装饰对象的名字,比如类名、属性名等。
      • addInitializer():函数,用来添加类的初始化逻辑。在类完全定义结束后执行。
      • private:布尔值,表示所装饰的对象是否为类的私有成员。
      • static:布尔值,表示所装饰的对象是否为类的静态成员。
      • access:一个对象,包含了某个值的 get 和 set 方法。

■ 类装饰器

        类装饰器接受两个参数:value(当前类本身)和context(上下文对象)。其中,context对象的kind属性固定为字符串class

        类装饰器一般用来对类进行操作,可以不返回任何值。

        类装饰器可以返回一个函数,替代当前类的构造方法

        类装饰器也可以返回一个新的类,替代原来所装饰的

■ 方法装饰器

        方法装饰器用来装饰类的方法(method)。方法装饰器会改写类的原始方法(.prototype上的)。

        方法装饰器是一个函数,接受两个参数:value和context。参数value是方法本身,参数context是上下文对象,有以下属性。

  • kind:值固定为字符串method,表示当前为方法装饰器。
  • name:所装饰的方法名,类型为字符串或 Symbol 值。
  • static:布尔值,表示是否为静态方法。该属性为只读属性。
  • private:布尔值,表示是否为私有方法。该属性为只读属性。
  • access:对象,包含了方法的存取器,但是只有get()方法用来取值,没有set()方法进行赋值。
  • addInitializer():为方法增加初始化函数。一个钩子方法,用来在类的初始化阶段添加回调函数。

        如果方法装饰器返回一个新的函数,就会替代所装饰的原始函数

        利用方法装饰器,可以将类的方法变成延迟执行。

■ 属性装饰器

        属性装饰器用来装饰定义在类顶部的属性(field)。

        注意,装饰器的第一个参数value的类型是undefined,这意味着这个参数实际上没用的,装饰器不能从value获取所装饰属性的值。另外,第二个参数context对象的kind属性的值为字符串field,而不是“property”或“attribute”,这一点是需要注意的。

        属性装饰器要么不返回值,要么返回一个函数,该函数会自动执行,用来对所装饰属性进行初始化。该函数的参数是所装饰属性的初始值,该函数的返回值是该属性的最终值

        属性装饰器的返回值函数,可以用来更改属性的初始值

        属性装饰器的上下文对象context的access属性,提供所装饰属性的存取器。

■ getter 装饰器,setter 装饰器

        属性装饰器的上下文对象context的access属性,提供所装饰属性的存取器

注意,getter 装饰器的上下文对象context的access属性,只包含get()方法;setter 装饰器的access属性,只包含set()方法。

        这两个装饰器要么不返回值,要么返回一个函数取代原来的取值器或存值器。

■ accessor 装饰器

class C {
  accessor x = 1; // accessor等同于为公开属性x自动生成取值器和存值器,它们作用于私有属性x
} 
//公开的x与私有的x不是同一个。也就是说,上面的代码等同于下面的代码。
class C {
  #x = 1;
  get x() {
    return this.#x;
  }
  set x(val) {
    this.#x = val;
  }
}

        accessor也可以与静态属性和私有属性一起使用。

        accessor 装饰器的value参数,是一个包含get()方法和set()方法的对象。

        该装饰器可以不返回值,或者返回一个新的对象,用来取代原来的get()方法和set()方法。

        此外,装饰器返回的对象还可以包括一个init()方法,用来改变私有属性的初始值

■ 装饰器的执行顺序

        装饰器的执行分为两个阶段。

  • 评估(evaluation):计算@符号后面的表达式的值,得到的应该是函数。
  • 应用(application):将评估装饰器后得到的函数,应用于所装饰对象。

        也就是说,装饰器的执行顺序是,先评估所有装饰器表达式的值,再将其应用于当前类

        应用装饰器时,顺序依次为方法装饰器和属性装饰器,然后是类装饰器。如果一个方法或属性有多个装饰器,则内层的装饰器先执行,外层的装饰器后执行。

18、装饰器(旧语法)

装饰器(旧语法) - TypeScript 教程 - 网道 (wangdoc.com)icon-default.png?t=N7T8https://wangdoc.com/typescript/decorator-legacy

19、declare 关键字

■ 简介

        declare 关键字用来告诉编译器,某个类型是存在的,可以在当前文件中使用。它的主要作用,就是让当前文件可以使用其他文件声明的类型。这样,编译单个脚本就不会因为使用了外部类型而报错。

        它只是通知编译器某个类型是存在的,不用给出具体实现只能用来描述已经存在的变量和数据结构,不能用来声明新的变量和数据结构,另外,所有declare语句都不会出现在编译后的文件里面

■ 参数

  • declare variable:给出外部变量的类型描述。(声明全局变量)
  • declare function:给出外部函数的类型描述。(声明全局方法)
  • declare class:给出 class 类型描述。(声明全局类)
  • declare module,declare namespace:把变量、函数、类组织在一起。
  • declare global:为 JavaScript 引擎的原生对象添加属性和方法。
  • declare enum:给出 enum 类型描述。(声明全局枚举类型)
  • declare module 用于类型声明文件:可以为每个模块脚本,定义一个.d.ts文件,把该脚本用到的类型定义都放在这个文件里面。但是,更方便的做法是为整个项目,定义一个大的.d.ts文件,在这个文件里面使用declare module定义每个模块脚本的类型。

20、d.ts 类型声明文件

■ 简介

        当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。

● declare var: 声明全局变量
● declare function: 声明全局方法
● declare class: 声明全局类
● declare enum: 声明全局枚举类型
● declare namespace: 声明(含有子属性的)全局对象
● interface 和 type: 声明全局类型
● export: 导出变量
● export namespace: 导出(含有子属性的)对象
● export default ES6: 默认导出
● export = commonjs: 导出模块
● export as namespace: UMD 库声明全局变量
● declare global: 扩展全局变量
● declare module: 扩展模块
● /// <reference />: 三斜线指令

        单独使用的模块,一般会同时提供一个单独的类型声明文件,把本模块的外部接口的所有类型都写在这个文件里面,便于模块使用者了解接口,也便于编译器检查使用者的用法是否正确。通常我们会把声明语句放到一个单独的文件。

        类型声明文件里面只有类型代码,没有具体的代码实现。它的文件名一般为[模块名].d.ts的形式,其中的d表示 declaration(声明)。

// 模块输出
module.exports = 3.142;// 整体导出
exports.bar = bar; // 单个导出
// 类型输出
// 写法一
declare const pi: number;
export default pi; // 模块输出在类型声明文件中,也可以使用export default表示
// 写法二
declare const pi: number;
export= pi; // 使用export =
//针对这种模块导出,有多种方式可以导入
// 写法一 const ... = require
const foo = require('foo'); // 整体导入
const bar = require('foo').bar; // 单个导入
// 写法二 import ... from
import * as foo from 'foo'; // 整体导入
import { bar } from 'foo'; // 单个导入
// 写法三 import ... require   ts 官方推荐的方式
import foo = require('foo'); // 整体导入
import bar = require('foo').bar; // 单个导入

■ 类型声明文件的来源

自动生成

        如果库的源码本身就是由 ts 写的,那么在使用 tsc 脚本将 ts 编译为 js 的时候,添加 declaration 选项,编译器就会在编译时自动生成单独的类型声明文件( .d.ts 声明文件)

{
  "compilerOptions": {
    "declaration": true
  }
}

        也可以在命令行打开这个选项。

tsc --declaration

内置声明文件

        安装TypeScript语言时,会同时安装一些内置的类型声明文件,主要是内置的全局对象(JavaScript语言接口和运行环境API)的类型声明。这些内置声明文件位于TypeScript语言安装目录的lib文件夹内。

外部类型声明文件

        外部模块的类型声明文件,需要自己安装。如果项目中使用了外部的某个第三方代码库,那么就需要这个库的类型声明文件。这时又分成三种情况。

  • 这个库自带了类型声明文件。一般来说,如果这个库的源码包含了[vendor].d.ts文件,那么就自带了类型声明文件,使用这个库可能需要单独加载它的类型声明文件。
  • 这个库没有自带,但是可以找到社区制作的类型声明文件。第三方库如果没有提供类型声明文件,社区往往会提供,TS社区主要使用 DefinitelyTyped 仓库,各种类型声明文件都会提交到那里,已经包含了几千个第三方库,这些声明文件都会作为一个单独的库,使用时安装这个库就可以了。
  • 找不到类型声明文件,需要自己写。有时实在没有第三方库的类型声明文件,又很难完整给出该库的类型描述,这时可以告诉 TypeScript 相关对象的类型是any。

■ declare 关键字

        类型声明文件只包含类型描述,不包含具体实现,所以非常适合使用 declare 语句来描述类型。

        类型声明文件里面,变量的类型描述必须使用declare命令,否则会报错,因为变量声明语句是值相关代码。

        interface 类型有没有declare都可以,因为 interface 是完全的类型代码。

        类型声明文件里面,顶层可以使用export命令,也可以不用,除非使用者脚本会显式使用export命令输入类型。

■ 模块发布

        当前模块如果包含自己的类型声明文件,可以在package.json文件里面添加一个types字段或typings字段,指明类型声明文件的位置。

        如果类型声明文件名为index.d.ts,且在项目的根目录中,就不需要在package.json里面注明了。

        当我们为一个库写好了声明文件之后,下一步就是将它发布出去了。此时有两种方案:

  • ① 将声明文件和源码放在一起(优先)。保持声明文件与源码在一起,使用时就不需要额外增加单独的声明文件库的依赖了,而且也能保证声明文件的版本与源码的版本保持一致。
  • ②将声明文件发布到@types下。仅当我们在给别人的仓库添加类型声明文件,但原作者不愿意合并 pull request 时,才需要使用第二种方案,将声明文件发布到 @types 下。

■ 三斜杠命令

        如果类型声明文件的内容非常多,可以拆分成多个文件,然后入口文件使用三斜杠命令,加载其他拆分后的文件。除了拆分类型声明文件,三斜杠命令也可以用于普通脚本加载类型声明文件

        三斜杠命令(///)是一个TypeScript编译器命令,用来指定编译器行为。只能用在文件的头部,如果用在其他地方,会被当作普通的注释。另外,若一个文件中使用了三斜线命令,那么在三斜线命令之前只允许使用单行注释、多行注释和其他三斜线命令,否则三斜杠命令也会被当作普通的注释。

/// <reference path="" />

        最常见的三斜杠命令,告诉编译器在编译时需要包括的文件,常用来声明当前脚本依赖的类型文件。

        编译器会在预处理阶段找出所有三斜杠引用的文件,将其添加到编译列表中,然后一起编译

        path参数指定了所引入文件的路径。如果该路径是一个相对路径,则基于当前脚本的路径进行计算。

        使用该命令时,有以下两个注意事项。

  • path参数必须指向一个存在的文件,若文件不存在会报错。
  • path参数不允许指向当前文件
默认情况下,每个三斜杠命令引入的脚本, 都会编译成单独的JS文件。如果希望编译后 只产出一个合并文件,可以使用编译选项 outFile。但是,outFile编译选项不支持合并CommonJS模块和ES模块,只有当编译参数module的值设为 None、System 或 AMD 时,才能编译成一个文件。
如果打开了编译参数 noResolve,则 忽略三斜杠指令。将其 当作一般的注释,原样保留在编译产物中。

/// <reference types="" />

        用来告诉编译器当前脚本依赖某个DefinitelyTyped类型库,通常安装在node_modules/@types目录。types 参数的值是类型库的名称,也就是安装到node_modules/@types目录中的子目录的名字。

        这个命令的作用类似于import命令。

        注意,这个命令只在自己手写类型声明文件(.d.ts文件)时,才有必要用到,就是说,只应该用在.d.ts文件中,普通的.ts脚本文件不需要写这个命令。如果是普通的.ts脚本,可以使用tsconfig.json文件的types属性指定依赖的类型库。

/// <reference lib="" />

        允许脚本文件显式包含内置 lib 库,等同于在tsconfig.json文件里面使用lib属性指定 lib 库。

        安装TypeScript软件包时,会同时安装一些内置的类型声明文件,即内置的lib库。这些库文件位于TypeScript安装目录的lib文件夹中,它们描述了JavaScript语言和引擎的标准 API。

库文件并不是固定的,会随着TS版本的升级而更新。库文件统一使用“lib.[description].d.ts”的命名方式,而/// <reference lib="" />里面的lib属性的值就是库文件名的description部分,比如lib="es2015"就表示加载库文件lib.es2015.d.ts。

21、类型运算符

■ keyof 运算符

简介

        keyof 是一个单目运算符,接受一个对象类型作为参数,返回该对象的所有键名组成的联合类型

        由于JS对象的键名只有三种类型,所以对于任意对象的键名的联合类型就是string|number|symbol

        对于没有自定义键名的类型使用 keyof 运算符,返回never类型,表示不可能有这样类型的键名。

type KeyT = keyof any; // string | number | symbol
type KeyT = keyof object; // never

        由于keyof返回的类型是string|number|symbol,如果有些场合只需要其中的一种类型,那么可以采用交叉类型的写法。type MyKeys<Obj extends object> = Capital<string & keyof Obj>;

        如果对象属性名采用索引形式,keyof 会返回属性名的索引类型

        如果 keyof 运算符用于数组或元组类型,会返回数组的所有键名,包括数字键名和继承的键名。

        对于联合类型,keyof 返回成员共有的键名

        对于交叉类型,keyof 返回所有键名

        keyof 取出的是键名组成的联合类型,如果想取出键值组成的联合类型,可以像下面这样写。

type MyObj = {
  foo: number,
  bar: string,
};
type Keys = keyof MyObj; // Keys是键名组成的联合类型
// MyObj[Keys]会取出每个键名对应的键值类型,组成一个新的联合类型
type Values = MyObj[Keys]; // number|string

keyof 运算符的用途

        keyof 运算符往往用于精确表达对象的属性类型。另一个用途是用于属性映射,即将一个类型的所有属性逐一映射成其他值。

■ in 运算符

        in运算符用来确定对象是否包含某个属性名

        TS语言的类型运算中,in运算符有不同的用法,用来取出(遍历)联合类型的每一个成员类型

■ 方括号运算符

        方括号运算符([])用于取出对象的键值类型,比如T[K]会返回对象T的属性K的类型。

        方括号的参数如果是联合类型,那么返回的也是联合类型

        如果访问不存在的属性,会报错

        方括号运算符的参数也可以是属性名的索引类型。这个语法对于数组也适用,可以使用number作为方括号的参数。

        注意,方括号里面不能有值的运算

■ extends...?: 条件运算符

        条件运算符 extends...?: 可以根据当前类型是否符合某种条件,返回不同的类型

/* extends用来判断,类型T是否可以赋值给类型U,即T是否为U的子类型,这里的T和U可以是任意类型。
   如果T能够赋值给类型U,表达式的结果为类型X,否则结果为类型Y。 */
T extends U ? X : Y

        如果需要判断的类型是一个联合类型,那么条件运算符会展开这个联合类型。

(A|B) extends U ? X : Y
// 等同于     相当于A和B分别进行运算符,返回结果组成一个联合类型
(A extends U ? X : Y) |
(B extends U ? X : Y)

        如果不希望联合类型被条件运算符展开,可以把extends两侧的操作数都放在方括号里面

// 示例一。类型参数是一个联合类型,所以会被展开,返回的也是联合类型
type ToArray<Type> =
  Type extends any ? Type[] : never;
type T = ToArray<string|number>; // string[]|number[]
// 示例二。两侧的运算数都放在方括号里面,所以传入的联合类型不会展开,返回的是一个数组
type ToArray<Type> =
  [Type] extends [any] ? Type[] : never;
type T = ToArray<string|number>; // (string | number)[]

        条件运算符还可以嵌套使用

■ infer 关键字

        infer关键字用来定义泛型里面推断出来的类型参数,而不是外部传入的类型参数。它通常跟条件运算符一起使用,用在extends关键字后面的父类型之中。

■ is 运算符

        函数返回布尔值的时候,可以使用is运算符,限定返回值与参数之间的关系。is运算符用来描述返回值属于true还是false

        is运算符总是用于描述函数的返回值类型,写法采用parameterName is Type的形式,即左侧为当前函数的参数名,右侧为某一种类型。它返回一个布尔值,表示左侧参数是否属于右侧的类型

        is运算符可以用于类型保护

        is运算符还有一种特殊用法,就是用在类(class)的内部,描述类的方法的返回值

■ 模板字符串

        TypeScript 允许使用模板字符串,构建类型。模板字符串的最大特点,就是内部可以引用其他类型。注意,模板字符串可以引用的类型一共7种,分别是 string、number、bigint、boolean、null、undefined、Enum。引用这7种以外的类型会报错。

        模板字符串里面引用的类型,如果是一个联合类型,那么它返回的也是一个联合类型,即模板字符串可以展开联合类型

        如果模板字符串引用两个联合类型,它会交叉展开这两个类型。

■ satisfies 运算符

        satisfies运算符用来检测某个值是否符合指定类型。有时候,不方便将某个值指定为某种类型,但是希望这个值符合类型条件,这时候就可以用satisfies运算符对其进行检测。

        satisfies可以检测属性名,也可以检测属性值

22、类型映射

■ 简介

        映射指的是,将一种类型按照映射规则,转换成另一种类型,通常用于对象类型

type A = {
  foo: number;
  bar: number;
};
type B = { // 类型B采用了属性名索引的写法
  [prop in keyof A]: string; // 得到类型A的所有属性名,然后将每个属性的类型改成string
};
/* 得到
type B = {
  foo: string;
  bar: string;
};
*/
type C = { // 类型C原样复制了类型A
  [prop in keyof A]: A[prop];
};

        在语法上,[prop in keyof A]是一个属性名表达式,表示这里的属性名需要计算得到。具体的计算规则如下:

  • prop:属性名变量,名字可以随便起。
  • in:运算符,用来取出右侧的联合类型的每一个成员。
  • keyof A:返回类型A的每一个属性名,组成一个联合类型。

        为了增加代码复用性,可以把常用的映射写成泛型

type ToBoolean<Type> = {
  [Property in keyof Type]: boolean;
}; // 定义了一个泛型,可以将其他对象的所有属性值都改成 boolean 类型

        不使用联合类型,直接使用某种具体类型进行属性名映射也是可以的。甚至还可写成p in string

type MyObj = {
  [p in 'foo']: number; // 可看成只有一个成员的联合类型,因此得到了只有这一个属性的对象类型
};
// 等同于
type MyObj = {
  foo: number;
};
type MyObj = {
  [p in string]: boolean; //[p in string]就是属性名索引形式[p: string]的映射写法
};
// 等同于
type MyObj = {
  [p: string]: boolean;
};

        通过映射,可以把某个对象的所有属性改成可选属性

type A = {
  a: string;
  b: number;
};
type B = { // 类型B在类型A的所有属性名后面添加问号,使得这些属性都变成了可选属性
  [Prop in keyof A]?: A[Prop];
};

        事实上,TS的内置工具类型Partial<T>,就是这样实现的。内置的工具类型Readonly<T>可以将所有属性改为只读属性,实现也是通过映射。

type Readonly<T> = {
  readonly [P in keyof T]: T[P]; // 将 T 的所有属性改为只读属性
};
// 它的用法如下
type T = { a: string; b: number };
type ReadonlyT = Readonly<T>;

■ 映射修饰符

        映射会原样复制原始对象的可选属性只读属性。TypeScript 引入了两个映射修饰符,用来在映射时添加或移除某个属性的?修饰符和readonly修饰符

  • + 修饰符:写成+?或+readonly,为映射属性添加?修饰符或readonly修饰符。
  • – 修饰符:写成-?或-readonly,为映射属性移除?修饰符或readonly修饰符。
  • 注意,+?或-?要写在属性名的后面+readonly和-readonly要写在属性名的前面
  • +?修饰符可以简写成?,+readonly修饰符可以简写成readonly
  • –?修饰符移除了可选属性以后,该属性就不能等于undefined了,实际变成必选属性了。但是,这个修饰符不会移除null类型
// 增加
type MyObj<T> = {
  +readonly [P in keyof T]+?: T[P];
};
// 移除
type MyObj<T> = {
  -readonly [P in keyof T]-?: T[P];
}

■ 键名重映射

语法

        键名重映射,允许改变键名。键名重映射的语法是在键名映射的后面加上as + 新类型子句。这里的“新类型”通常是一个模板字符串,里面可以对原始键名进行各种操作。

type A = {
  foo: number;
  bar: number;
};
type B = {
  [p in keyof A as `${p}ID`]: number;
};
// 等同于
type B = {
  fooID: number;
  barID: number;
};

属性过滤

        键名重映射还可以过滤掉某些属性

type User = {
  name: string,
  age: number
}
type Filter<T> = { // 过滤不符合条件的属性,只保留属性值为字符串的属性
  [K in keyof T // 映射K in keyof T获取类型T的每一个属性以后,然后使用as Type修改键名
    as T[K] extends string ? K : never]: string 
}  // 如果属性值T[K]的类型是字符串,那么属性名不变,否则属性名类型改为never(该属性名不存在)
type FilteredUser = Filter<User> // { name: string }

联合类型的映射

        由于键名重映射可以修改键名类型,所以原始键名的类型不必是string|number|symbol,任意的联合类型都可以用来进行键名重映射

23、类型工具

TypeScript 类型工具 - TypeScript 教程 - 网道 (wangdoc.com)icon-default.png?t=N7T8https://wangdoc.com/typescript/utility

24、注释指令

TypeScript 的注释指令 - TypeScript 教程 - 网道 (wangdoc.com)icon-default.png?t=N7T8https://wangdoc.com/typescript/comment

25、tsconfig.json 文件

tsconfig.json - TypeScript 教程 - 网道 (wangdoc.com)icon-default.png?t=N7T8https://wangdoc.com/typescript/tsconfig.json

26、tsc 命令

■ 简介

        tsc是TypeScript官方的命令行编译器,用来检查代码,并将其编译成JavaScript代码

        tsc默认使用当前目录下的配置文件tsconfig.json,但也可以接受独立的命令行参数命令行参数会覆盖tsconfig.json,比如命令行指定了所要编译的文件,那么 tsc 就会忽略tsconfig.json的files属性。

# 使用 tsconfig.json 的配置
$ tsc
# 只编译 index.ts
$ tsc index.ts
# 编译 src 目录的所有 .ts 文件
$ tsc src/*.ts
# 指定编译配置文件
$ tsc --project tsconfig.production.json
# 只生成类型声明文件,不编译出 JS 文件
$ tsc index.js --declaration --emitDeclarationOnly
# 多个 TS 文件编译成单个 JS 文件
$ tsc app.ts util.ts --target esnext --outfile index.js

■ 命令行参数

tsc 命令行编译器 - TypeScript 教程 - 网道 (wangdoc.com)icon-default.png?t=N7T8https://wangdoc.com/typescript/tsc

tsc 的命令行参数,大部分与 tsconfig.json 的属性一一对应。

详细解释可以参考《tsconfig.json 配置文件》

  • 13
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值