TypeScript 知识要点汇总

656 阅读59分钟

TypeScript 知识要点汇总

TypeScript基础

1. 数据类型

TypeScript 中的类型有:

  • 原始类型

    • boolean
    • number
    • string
    • void**(当一个函数没有返回值时,可以将其返回值类型定义为 void:声明一个 void 类型的变量没有什么用,因为你只能将它赋值为 undefinednull:)**
    • null**(undefinednull 是所有类型的子类型。)**
    • undefined
    • bigint
    • symbol Symbol() 函数会返回 symbol 类型的值。每个从 Symbol() 返回的 symbol 值都是唯一的。)
  • 元组 tuple (相同类型元素组成成为数组,不同类型元素组成了元组(Tuple)。)

  • 枚举 enum

  • 任意 any (any类型会跳过类型检测,可被赋予任何类型,并且如果一个数据是any类型,那么可以访问它的任意属性,即使这个属性不存在)

  • unknown (unknown类型可被赋予任何类型的值,但是unknown 类型和any类型的不同点在于,unknown类型在被确定为某个类型之前,不能被进行诸如函数执行、实例化等操作,一定程度上对类型进行了保护。)

  • never never 类型表示那些永不存在的值的类型,一般在抛出异常时使用。never 类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是 never 的子类型或可以赋值给 never 类型(除了 never 本身之外)。 即使 any 也不可以赋值给 never。)

  • 数组 Array

  • 对象 object object 表示非原始类型,枚举、数组、元组和普通对象都是 object 类型。)

2.bigint数据类型

定义:

bigint 是一种基本数据类型(primitive data type)。 bigint 数据类型是用来表示那些已经超出了 number 类型最大值的整数值,对于总是被诟病的整数溢出问题,使用了 bigint 后将完美解决。JavaScript 中可以用 Number 表示的最大整数为 2^53 - 1,可以写为 Number.MAX_SAFE_INTEGER。如果超过了这个界限,可以用 BigInt来表示,它可以表示任意大的整数。

语法:

​ 在一个整数字面量后加 n 的方式定义一个 BigInt,如:10n 或者调用函数 BigInt(),这里注意bigint是基本数据类型,调用构造函数生成的时候不能使用new BigInt():

const theBiggestInt: bigint = 9007199254740991n
const alsoHuge: bigint = BigInt(9007199254740991)
const hugeString: bigint = BigInt("9007199254740991")

theBiggestInt === alsoHuge // true
theBiggestInt === hugeString // true

BigIntNumber 的不同点:

  • BigInt 不能用于 Math 对象中的方法。
  • BigInt 不能和任何 Number 实例混合运算,两者必须转换成同一种类型。
  • BigInt 变量在转换为 Number 变量时可能会丢失精度。
const biggest: number = Number.MAX_SAFE_INTEGER

const biggest1: number = biggest + 1
const biggest2: number = biggest + 2

biggest1 === biggest2 // true 超过精度

类型信息

使用 typeof 检测类型时,BigInt 对象返回 bigint:

typeof 10n === 'bigint'         // true
typeof BigInt(10) === 'bigint'  // true

typeof 10 === 'number'          // true
typeof Number(10) === 'number'  // true

运算

BigInt 可以正常使用 +-*/**% 符号进行运算:

const previousMaxSafe: bigint = BigInt(Number.MAX_SAFE_INTEGER)  // 9007199254740991n

const maxPlusOne: bigint = previousMaxSafe + 1n                  // 9007199254740992n

const multi: bigint = previousMaxSafe * 2n                       // 18014398509481982n

const subtr: bigint = multi – 10n                                // 18014398509481972n

const mod: bigint = multi % 10n                                  // 2n

const bigN: bigint = 2n ** 54n                                   // 18014398509481984n

const divided: bigint = 5n / 2n                                   // 2n , 当使用 / 操作符时,会向下取整,不会返回小数部分:

比较与条件判断

NumberBigInt 可以进行比较:

0n === 0 // false

0n == 0 // true

1n < 2  // true

2n > 1  // true

2n >= 2 // true

条件判断:

0n || 10n    // 10n

0n && 10n    // 0n

Boolean(0n)  // false

Boolean(10n) // true

!10n         // false

!0n          // true

3.枚举(Enum)数据类型

**定义:**使用枚举我们可以定义一些带名字的常量。TypeScript 支持数字的和基于字符串的枚举。

数字枚举和字符串枚举:

​ 声明一个枚举类型,如果没有赋值,它们的值默认为数字类型且从 0 开始累加:

enum Months {
  Jan,
  Feb,
  Mar,
  Apr
}

Months.Jan === 0 // true
Months.Feb === 1 // true
Months.Mar === 2 // true
Months.Apr === 3 // true

现实中月份是从 1 月开始的,那么只需要这样:

// 从第一个数字赋值,往后依次累加
enum Months {
  Jan = 1,
  Feb,
  Mar,
  Apr
}

Months.Jan === 1 // true
Months.Feb === 2 // true
Months.Mar === 3 // true
Months.Apr === 4 // true

若枚举类型的值为字符串类型

enum TokenType {
  ACCESS = 'accessToken',
  REFRESH = 'refreshToken'
}

// 两种不同的取值写法
console.log(TokenType.ACCESS === 'accessToken')        // true
console.log(TokenType['REFRESH'] === 'refreshToken')   // true

数字类型和字符串类型可以混合使用,但是不建议:

enum BooleanLikeHeterogeneousEnum {
    No = 0,
    Yes = "YES",
}

**计算常量成员:**枚举类型的值可以是一个简单的计算表达式:

enum Calculate {
  a,
  b,
  expired = 60 * 60 * 24,
  length = 'imooc'.length,
  plus = 'hello ' + 'world'
}

console.log(Calculate.expired)   // 86400
console.log(Calculate.length)    // 5
console.log(Calculate.plus)      // hello world

Tips:

  • 计算结果必须为常量。
  • 计算项必须放在最后。

**反向映射:**所谓的反向映射就是指枚举的取值,不但可以正向的 Months.Jan 这样取值,也可以反向的 Months[1] 这样取值。

enum Months {
  Jan = 1,
  Feb,
  Mar,
  Apr
}

将上面的代码进行编译,查看编译后的 JavaScript 代码:

'use strict'
var Months;
(function (Months) {
  Months[Months['Jan'] = 1] = 'Jan'
  Months[Months['Feb'] = 2] = 'Feb'
  Months[Months['Mar'] = 3] = 'Mar'
  Months[Months['Apr'] = 4] = 'Apr'
})(Months || (Months = {}))

通过查看编译后的代码,可以得出:

console.log(Months.Mar === 3) // true

// 那么反过来能取到 Months[3] 的值吗?
console.log(Months[3])  // 'Mar'

// 所以
console.log(Months.Mar === 3)     // true
console.log(Months[3] === 'Mar')  // true

Tips:

  1. 数字枚举成员可以生成反向映射,但是字符串枚举成员不会生成反向映射!!!
  2. 枚举类型被编译成一个对象,它包含了正向映射( name -> value)和反向映射( value -> name)。

**const 枚举:**在枚举上使用 const 修饰符:

enum Months {
  Jan = 1,
  Feb,
  Mar,
  Apr
}

const month = Months.Mar

查看一下编译后的内容:

'use strict'
const month = 3 /* Mar */

发现枚举类型应该编译出的对象没有了,只剩下 month 常量。这就是使用 const 关键字声明枚举的作用。因为变量 month 已经使用过枚举类型,在编译阶段 TypeScript 就将枚举类型抹去,这也是性能提升的一种方案。

**枚举合并:**分开声明名称相同的枚举类型,会自动合并

enum Months {
  Jan = 1,
  Feb,
  Mar,
  Apr
}

enum Months {
  May = 5,
  Jun
}

console.log(Months.Apr) // 4
console.log(Months.Jun) // 6

编译后的 JavaScript 代码:

'use strict'
var Months;
(function (Months) {
  Months[Months['Jan'] = 1] = 'Jan'
  Months[Months['Feb'] = 2] = 'Feb'
  Months[Months['Mar'] = 3] = 'Mar'
  Months[Months['Apr'] = 4] = 'Apr'
})(Months || (Months = {}));
(function (Months) {
  Months[Months['May'] = 5] = 'May'
  Months[Months['Jun'] = 6] = 'Jun'
})(Months || (Months = {}))

console.log(Months.Apr) // 4
console.log(Months.Jun) // 6

4.数组的表示方法

  1. 元素类型后接上[]:

    let list: number[] = [1, 2, 3]
    let names: string[] = ['Sherlock', 'Watson', 'Mrs. Hudson']
    
    
  2. 使用数组泛型,Array<元素类型>

    let list: Array<number> = [1, 2, 3]
    let names: Array<string> = ['Sherlock', 'Watson', 'Mrs. Hudson']
    
    

​ 3. 使用可索引类型的接口:

interface ScenicInterface {
  [index: number]: string
}

let arr: ScenicInterface = ['西湖', '华山', '故宫']
let favorite: string = arr[0]

5. 接口(Interface)

**定义:**接口是对 JavaScript 本身的随意性进行约束,通过定义一个接口,约定了变量、类、函数等应该按照什么样的格式进行声明,实现多人合作的一致性。TypeScript 编译器依赖接口用于类型检查,最终编译为 JavaScript 后,接口将会被移除。

**语法:**如果没有特殊声明,例如下面的可选属性,任意属性等,定义的变量比接口少了一些属性是不允许的,多一些属性也是不允许的,赋值的时候,变量的形状必须和接口的形状保持一致。

// 语法格式,接口名首字母以及后面每个单词的首字母都要大写!
interface DemoInterface {
    name:string;
    age?:number;   //属性后面使用?定义一个可选属性
    readonly id:number;  //属性前面使用readonly关键字定义只读属性
    like: ReadonlyArray<number>  //通过 ReadonlyArray<T> 设置数组为只读,它的所有写方法都会失效。
    [propName: string]: any;  //表示任意属性,用 [] 将属性包裹起来,[]里面的属性名不一定是propName,可以随意,propName:后面跟的是属性名的类型,[]外面的:后面跟的是属性值的数据类型。
    fn1(source: string, subString: string): boolean; //接口定义一个函数,只要参数和返回值的类型符合就允许,参数名字不一定必须为source和subString,可以为任意,但是必须保证顺序的一致性。
    fn2(source: string, subString: string) => boolean; // fn1和fn2两种定义函数的方式是等价的,要注意的是在接口定义中,函数后面的=>并非箭头函数,而是函数定义中的返回标识符,后面紧跟的是返回值的类型。
}

要注意的是接口中还可以定义一个匿名函数,对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配。你可以改变函数的参数名,只要保证函数参数的位置不变。函数的参数会被逐个进行检查:

interface SearchFunc {
  (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string): boolean {
  return source.search(subString) > -1;
}

**可索引类型:**可索引类型接口读起来有些拗口,直接看例子:

// 正常的js代码
let arr = [1, 2, 3, 4, 5]
let obj = {
  brand: 'imooc',
  type: 'education'
}

arr[0]
obj['brand']

再来看定义可索引类型接口:

interface ScenicInterface {
  [index: number]: string
}

let arr: ScenicInterface = ['西湖', '华山', '故宫']
let favorite: string = arr[0]

示例中索引签名是 number类型,返回值是字符串类型。

另外还有一种索引签名是 字符串类型。我们可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。通过下面的例子理解这句话:

// 正确
interface Foo {
  [index: string]: number;
  x: number;
  y: number;
}

// 错误
interface Bar {
  [index: string]: number;
  x: number;
  y: string; // Error: y 属性必须为 number 类型
}

代码解释:

第 12 行,语法错误是因为当使用 number 来索引时,JavaScript 会将它转换成 string 然后再去索引对象。也就是说用 100(一个number)去索引等同于使用"100"(一个string)去索引,因此两者需要保持一致。

**类类型:**我们希望类的实现必须遵循接口定义,那么可以使用 implements 关键字来确保兼容性。这种接口与抽象类比较相似,但是接口只能含有抽象方法和成员属性,实现类中必须实现接口中所有的抽象方法和成员属性。

interface AnimalInterface {
  name: string

  eat(m: number): string
}

class Dog implements AnimalInterface {
  name: string;

  constructor(name: string){
    this.name = name
  }

  eat(m: number) {
    return `${this.name}吃肉${m}分钟`
  }
}

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

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

interface Shape {
  color: string;
}

interface PenStroke {
  penWidth: number;
}

interface Square extends Shape, PenStroke {
  sideLength: number;
}

let square = {} as Square;
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

6.类(Class)

**访问修饰符:**TypeScript 可以使用四种访问修饰符 public、protected、private 和 readonly。

  • **public :**TypeScript 中,类的成员全部默认为 public,当然你也可以显式的将一个成员标记为 public,标记为 public 后,在程序类的外部可以访问。
  • protected:当成员被定义为 protected 后,只能被类的内部以及类的子类访问
  • private :当类的成员被定义为 private 后,只能被类的内部访问
  • **readonly:**通过 readonly 关键字将属性设置为只读的。只读属性必须在声明时或构造函数里被初始化。
class Parent{
  constructor() { 
  
  }
    
  protected drink() {
    console.log('drink')
  }
    
  private eat() {
    console.log('eat')
  }
}

class Children extends Parent {
  readonly secret: string = 'xjx*xh3GzW#3' //只读属性必须在声明时或构造函数里被初始化

  readonly expired: number
    
  constructor(expired: number) { 
    // 调用父类的构造函数
    super()  
    this.expired = expired  //只读属性必须在声明时或构造函数里被初始化
  }
  
  childrenDrik() {
     this.drink()  //ok,这是父类的保护成员,可以被子类访问。
  }
  
  childrenEet(){
     this.eat() //error,这是父类的私有成员,只能被父类的内部访问
  }	
}

静态方法:通过 static 关键字来创建类的静态成员,这些属性存在于类本身上面而不是类的实例上

class User {
  static getInformation () {
    return 'This guy is too lazy to write anything.'
  }
}

User.getInformation() // OK

const user = new User()
user.getInformation() // Error 实例中无此方法

静态方法调用同一个类中的其他静态方法,可使用 this 关键字。

class StaticMethodCall {

  static staticMethod() {
      return 'Static method has been called'
  }
  static anotherStaticMethod() {
      return this.staticMethod() + ' from another static method'
  }

}

代码解释: 静态方法中的 this 指向类本身,而静态方法也存在于类本身,所以可以在静态方法中用 this 访问在同一类中的其他静态方法。

非静态方法中,不能直接使用 this 关键字来访问静态方法。而要用类本身或者构造函数的属性来调用该方法:

class StaticMethodCall {
  constructor() {
      // 类本身调用
      console.log(StaticMethodCall.staticMethod())

      // 构造函数的属性调用
      console.log(this.constructor.staticMethod())
  }
  static staticMethod() {
      return 'static method has been called.'
  }
}

代码解释: 类指向其构造函数本身,在非静态方法中,this.constructor === StaticMethodCalltrue, 也就是说这两种写法等价。

**抽象类:**抽象类作为其它派生类的基类使用,它们一般不会直接被实例化,不同于接口,抽象类可以包含成员的实现细节。

abstract 关键字是用于定义抽象类和在抽象类内部定义抽象方法。

abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log('roaming the earch...');
    }
}

const animal = new Animal() // Error, 无法创建抽象类实例

通常我们需要创建子类继承抽象类,将抽象类中的抽象方法一一实现,这样在大型项目中可以很好的约束子类的实现。

class Dog extends Animal {
  makeSound() {
    console.log('bark bark bark...')
  }
}

const dog = new Dog()

dog.makeSound()  // bark bark bark...
dog.move()       // roaming the earch...

把类当做接口:

类也可以作为接口来使用,这在项目中是很常见的。

class Pizza {
  constructor(public name: string, public toppings: string[]) {}
}

class PizzaMaker {
  // 把 Pizza 类当做接口
  static create(event: Pizza) {
    return new Pizza(event.name, event.toppings)
  }
}

const pizza = PizzaMaker.create({ 
  name: 'Cheese and nut pizza', 
  toppings: ['pasta', 'eggs', 'milk', 'cheese']
})

第 7 行,把 Pizza 类当做接口。

因为接口和类都定义了对象的结构,在某些情况下可以互换使用。如果你需要创建一个可以自定义参数的实例,同时也可以进行类型检查,把类当做接口使用不失为一个很好的方法。

7.函数(Function)

**函数类型:**在 TypeScript 中编写函数,需要给形参和返回值指定类型:

const add: (x: number, y: number) => string = function(x: number, y: number): string {
  return (x + y).toString()
}

可以看到,等号左侧的类型定义由两部分组成:参数类型和返回值类型,通过 => 符号来连接。

这里要注意:函数类型的 => 和 箭头函数的 => 是不同的含义

通过箭头函数改写一下刚才写的函数:

const add = (x: number, y: number): string => (x + y).toString()

等号左右两侧书写完整:

// 只要参数位置及类型不变,变量名称可以自己定义,比如把两个参数定位为 a b
const add: (a: number, b: number) => string = (x: number, y: number): string => (x + y).toString()

**函数的参数:**TypeScript 中每个函数参数都是必须的。 这不是指不能传递 null 或 undefined 作为参数,而是说编译器会检查用户是否为每个参数都传入了值。简短地说,传递给一个函数的参数个数必须与函数期望的参数个数一致。

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

const fullName = (firstName: string, lastName?: string): string => `${firstName}${lastName}`

let result1 = fullName('Sherlock', 'Holmes')
let result2 = fullName('Sherlock', 'Holmes', 'character') // Error, Expected 1-2 arguments, but got 3
let result3 = fullName('Sherlock')                        // OK

代码解释:

第 1 行,firstName 是必须参数,lastName 是可选参数。

第 4 行,传入了 3 个参数,与声明的 2 个参数不符。

第 5 行,lastName 是可选参数,可以省略。

默认参数:参数可以取默认值,上面介绍的可选参数必须跟在必须参数后面,而带默认值的参数不需要放在必须参数的后面,可随意调整位置

const token = (expired = 60*60, secret: string): void  => {}
// 或
const token1 = (secret: string, expired = 60*60 ): void => {}

代码解释:

第 1 行,带默认值的参数 expired 在参数列表首位。

第 3 行,带默认值的参数 expired 在参数列表末位。

**剩余参数:**有的时候,函数的参数个数是不确定的,可能传入未知个数,这时没有关系,有一种方法可以解决这个问题。通过 rest 参数 (形式为 ...变量名)来获取函数的剩余参数,这样就不需要使用 arguments 对象了。

function assert(ok: boolean, ...args: string[]): void {
  if (!ok) {
    throw new Error(args.join(' '));
  }
}

assert(false, '上传文件过大', '只能上传jpg格式')

代码解释:

第 1 行,第二个参数传入剩余参数,且均为字符串类型。

第 7 行,调用函数 assert() 时,除了第一个函数传入一个布尔类型,接下来可以无限传入多个字符串类型的参数。

TIP:注意 rest 参数 只能是最后一个参数。

**this 参数:**JavaScript 里,this 的值在函数被调用的时候才会被指定,this指向调用这个函数的对象。

默认情况下,tsconfig.json 中,编译选项 compilerOptions 的属性 noImplicitThisfalse,我们在一个对象中使用的 this 时,它的类型是 any 类型。在实际工作中 any 类型是非常危险的,我们可以添加任意属性到 any 类型的参数上。而在 tsconfig.json 中,将编译选项 compilerOptions 的属性 noImplicitThis 设置为 true,TypeScript 编译器就会帮你进行正确的类型推断。

**函数重载:**函数重载是指函数根据传入不同的参数,返回不同类型的数据。

它的意义在于让你清晰的知道传入不同的参数得到不同的结果,如果传入的参数不同,但是得到相同类型的数据,那就不需要使用函数重载。

比如面试中常考的字符反转问题,这里就不考虑负数情况了,只是为了演示函数重载:

function reverse(target: string | number) {
  if (typeof target === 'string') {
    return target.split('').reverse().join('')
  }
  if (typeof target === 'number') {
    return +[...target.toString()].reverse().join('')
  }
}

console.log(reverse('imooc'))   // coomi
console.log(reverse(23874800))  // 847832

编译器并不知道入参是什么类型的,返回值类型也不能确定。这时可以为同一个函数提供多个函数类型定义来进行函数重载。

(通过 --downlevelIteration 编译选项增加对生成器和迭代器协议的支持)

function reverse(x: string): string
function reverse(x: number): number

function reverse(target: string | number) {
  if (typeof target === 'string') {
    return target.split('').reverse().join('')
  }
  if (typeof target === 'number') {
    return +[...target.toString()].reverse().join('')
  }
}
console.log(reverse('imooc'))   // coomi
console.log(reverse(23874800))  // 847832

代码解释:

因为这个反转函数在传入字符串类型的时候返回字符串类型,传入数字类型的时候返回数字类型,所以在前两行进行了两次函数类型定义。在函数执行时,根据传入的参数类型不同,进行不同的计算。

为了让编译器能够选择正确的检查类型,它会从重载列表的第一个开始匹配。因此,在定义重载时,一定要把最精确的定义放在最前面

8.字面量类型

**定义:**字面量(literal)是用于表达源代码中一个固定值的表示法(notation)。通俗的讲,字面量也可以叫直接量,就是你看见什么,它就是什么。比如:你定义为 'imooc',那这个变量的类型就是 'imooc' 类型。字面量类型分为字符串字面量类型、布尔字面量类型和数字字面量类型。

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

let protagonist: 'Sherlock'

protagonist = 'Sherlock'
protagonist = 'Watson' // Error, Type '"Watson"' is not assignable to type '"Sherlock"'

代码解释: 变量 protagonist 被声明为 'Sherlock' 字面量类型,就只能赋值为 'Sherlock'

**布尔字面量类型:**声明布尔字面量类型,注意这里是 : 不是 == 等号是变量赋值,: 表示声明的类型。

let success: true
let fail: false
let value: true | false

接口的返回值,会有正确返回和异常两种情况,这两种情况要有不同的数据返回格式:

type Result = { success: true, code: number, object: object } | { success: false, code: number, errMsg: string }

let res: Result = { success: false, code: 90001, errMsg: '该二维码已使用' }

if (!res.success) {
  res.errMsg // OK
  res.object // Error, Property 'object' does not exist on type '{ success: false; code: number; errMsg: string; }
}

代码解释:

类型别名 Result 是一个由两个对象组成的联合类型,都有一个共同的 success 属性,这个属性的类型就是布尔字面量类型。

**数字字面量类型:**就是定义的类似是number类型的具体值

比如骰子只有六种点数:

let die: 1 | 2 | 3 | 4 | 5 | 6

die = 9 // Error
die=6  //ok

9.类型推断

**定义:**类型推断的含义是不需要指定变量类型或函数的返回值类型,TypeScript 可以根据一些简单的规则推断其的类型。TypeScript的类型推断分为:从右到左的类型推断(基础类型推断,最佳通用类型推断)和从左到右类型推断(上下文类型推断)

**基础类型推断:**基础的类型推断发生在 初始化变量,设置默认参数和决定返回值时。

初始化变量例子:

let x = 3             // let x: number
let y = 'hello world' // let y: string

let z                 // let z: any

代码解释:

变量 x 的类型被推断为数字,变量 y 的类型被推断为字符串。如果定义时没有赋值,将被推断为 any 类型。

设置默认参数和决定返回值时的例子:

// 返回值推断为 number
function add(a:number, b:10) {
  return a + b
}

const obj = {
  a: 10,
  b: 'hello world'
}

obj.b = 15 // Error,Type '15' is not assignable to type 'string'

代码解释:

第 1 行,参数 b 有默认值 10,被推断为 number 类型。

第 2 行,两个 number 类型相加,函数 add() 返回值被推断为 number 类型。

最后一行,obj 的类型被推断为 {a: number, b: string},所以属性 b 不能被赋值为数字。

const obj = {
  protagonist: 'Sherlock',
  gender: 'male'
}

let { protagonist } = obj

代码解释: 通过解构赋值也可以完成正确的类型推断:let protagonist: string

**最佳通用类型推断:**当需要从多个元素类型推断出一个类型时,TypeScript 会尽可能推断出一个兼容所有类型的通用类型。

比如声明一个数组:

let x = [1, 'imooc', null]

代码解释:为了推断 x 的类型,必须考虑所有的元素类型。这里有三种元素类型 number、string 和 null,此时数组被推断为 let x: (string | number | null)[] 联合类型。

Tip: 是否兼容 null 类型可以通过 tsconfig.json 文件中属性 strictNullChecks 的值设置为 true 或 false 来决定。

**上下文类型推断:**前面两种都是根据从右向左流动进行类型推断,上下文类型推断则是从左向右的类型推断。

例如定义一个 Animal 的类作为接口使用:

class Animal {
  public species: string | undefined
  public weight: number | undefined
}

const simba: Animal = {
  species: 'lion',
  speak: true  // Error, 'speak' does not exist in type 'Animal'
}

代码解释: 第 6 行,将 Animal 类型显示的赋值给 变量 simbaAnimal 类型 没有 speak 属性,所以不可赋值。

10.类型断言

**定义:**TypeScript 允许你覆盖它的推断,毕竟作为开发者你比编译器更了解你写的代码。类型断言主要用于当 TypeScript 推断出来类型并不满足你的需求,你需要手动指定一个类型。

断言方式:

1.关键字as

当你把 JavaScript 代码迁移到 TypeScript 时,一个常见的问题:

const user = {}

user.nickname = 'Evan'  // Error, Property 'nickname' does not exist on type '{}'
user.admin = true       // Error, Property 'admin' does not exist on type '{}'

代码解释: 编译器推断 const user: {},这是一个没有属性的对象,所以你不能对其添加属性。

此时可以使用类型断言(as关键字)覆盖其类型推断:

interface User {
  nickname: string,
  admin: boolean,
  groups: number[]
}

const user = {} as User

user.nickname = 'Evan' 
user.admin = true       
user.groups = [2, 6]

代码解释:

第 7 行,这里通过 as 关键字进行类型断言,将变量 user 的类型覆盖为 User 类型。但是请注意,类型断言不要滥用,除非你完全明白你在干什么。

2.首尾标签

类型断言还可以通过标签 <> 来实现:

interface User {
  nickname: string,
  admin: boolean,
  groups: number[]
}

const user = <User>{} // User类型

user.nickname = 'Evan' 
user.admin = true       
user.groups = [2, 6]

代码解释:

第 7 行,使用 <User>{} 这种标签形式,将变量 user 强制断言为 User 类型。

但是,当你在使用 JSX 语法时,会跟标签 <> 形式的类型断言混淆:

let nickname = <User>Evan</User>  // 这里的 User 指向一个 component

所以,建议统一使用 as type 这种语法来为类型断言。

非空断言 !

如果编译器不能够去除 null 或 undefined,可以使用非空断言 ! 手动去除。

function fixed(name: string | null): string {
  function postfix(epithet: string) {
    return name!.charAt(0) + '.  the ' + epithet; // name 被断言为非空
  }
  name = name || "Bob"
  return postfix("great")
}

代码解释:

第 2 行,postfix() 是一个嵌套函数,因为编译器无法去除嵌套函数的 null (除非是立即调用的函数表达式),所以 TypeScript 推断第 3 行的 name 可能为空。

第 5 行,而 name = name || "Bob" 这行代码已经明确了 name 不为空,所以可以直接给 name 断言为非空(第 3 行)。

双重断言

双重断言极少有应用场景,只需要知道有这种操作即可:

interface User {
  nickname: string,
  admin: boolean,
  group: number[]
}

const user = 'Evan' as any as User

代码解释: 最后一行,使用 as 关键字进行了两次断言,最终变量 user 被强制转化为 User 类型。

11.类型保护

本节介绍的类型保护 TypeScript 类型检查机制的第二个部分,我们可以通过 typeofinstanceofin字面量类型 将代码分割成范围更小的代码块,在这一小块中,变量的类型是确定的。

**定义:**类型保护是指缩小类型的范围,在一定的块级作用域内由编译器推导其类型,提示并规避不合法的操作。

**typeof:**通过 typeof 运算符判断变量类型,来达到缩小类型范围的效果

function reverse(target: string | number) {
  if (typeof target === 'string') {
    target.toFixed(2) // Error,在这个代码块中,target 是 string 类型,没有 toFixed 方法
    return target.split('').reverse().join('')
  }
  if (typeof target === 'number') {
    target.toFixed(2) // OK
    return +[...target.toString()].reverse().join('')
  }

  target.forEach(element => {}) // Error,在这个代码块中,target 是 string 或 number 类型,没有 forEach 方法
}

代码解释:

第 2 行,通过 typeof 关键字,将这个代码块中变量 target 的类型限定为 string 类型。

第 6 行,通过 typeof 关键字,将这个代码块中变量 target 的类型限定为 number 类型。

第 11 行,因没有限定,在这个代码块中,变量 target 是 string 或 number 类型,没有 forEach 方法,所以报错。

**instanceof:**instanceof 与 typeof 类似,区别在于 typeof 判断基础类型,instanceof 判断是否为某个对象的实例:

class User {
  public nickname: string | undefined
  public group: number | undefined
}

class Log {
  public count: number = 10
  public keyword: string | undefined
}

function typeGuard(arg: User | Log) {
  if (arg instanceof User) {
    arg.count = 15 // Error, User 类型无此属性
  }

  if (arg instanceof Log) {
    arg.count = 15 // OK
  }
}

代码解释:

第 12 行,通过 instanceof 关键字,将这个代码块中变量 arg 的类型限定为 User 类型。

第 16 行,通过 instanceof 关键字,将这个代码块中变量 arg 的类型限定为 Log 类型。

in:in 操作符用于确定属性是否存在于某个对象上,这也是一种缩小范围的类型保护。

class User {
  public nickname: string | undefined
  public groups!: number[]
}

class Log {
  public count: number = 10
  public keyword: string | undefined
}

function typeGuard(arg: User | Log) {
  if ('nickname' in arg) {
    // (parameter) arg: User,编辑器将推断在当前块作用域 arg 为 User 类型
    arg.nickname = 'imooc'
  }

  if ('count' in arg) {
    // (parameter) arg: Log,编辑器将推断在当前块作用域 arg 为 Log 类型
    arg.count = 15
  }
}

代码解释:

第 12 行,通过 in 关键字,将这个代码块中变量 arg 的类型限定为 User 类型。

第 17 行,通过 in 关键字,将这个代码块中变量 arg 的类型限定为 Log 类型。

**字面量类型保护:**通过字面量来缩小变量的类型的范围

type Success = {
  success: true,
  code: number,
  object: object
}

type Fail = {
  success: false,
  code: number,
  errMsg: string,
  request: string
}

function test(arg: Success | Fail) {
  if (arg.success === true) {
    console.log(arg.object) // OK
    console.log(arg.errMsg) // Error, Property 'errMsg' does not exist on type 'Success'
  } else {
    console.log(arg.errMsg) // OK
    console.log(arg.object) // Error, Property 'object' does not exist on type 'Fail'
  }
}

代码解释:

第 15 行,通过布尔字面量,将这个代码块中变量 arg 的类型限定为 Success 类型。

第 18 行,通过布尔字面量,将这个代码块中变量 arg 的类型限定为 Fail 类型。

TypeScript 进阶

1.泛型(Generic)

**定义:**泛型在传统的面向对象语言中极为常见,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。

通俗来讲:泛型是指在定义函数、接口或者类时,未指定其参数类型,只有在运行时传入才能确定。那么此时的参数类型就是一个变量,通常用大写字母 T 来表示,当然你也可以使用其他字符,如:UK等。

语法:在函数名、接口名或者类名以及类型别名添加后缀 <T>

function generic<T>() {}
interface Generic<T> {}
class Generic<T> {}
type type1<T> = 'string'| 'number'

初识泛型

之所以使用泛型,是因为它帮助我们为不同类型的输入,复用相同的代码。

比如写一个最简单的函数,这个函数会返回任何传入它的值。如果传入的是 number 类型:

function identity(arg: number): number {
    return arg
}

如果传入的是 string 类型:

function identity(arg: string): string {
    return arg
}

通过泛型,可以把两个函数统一起来:

function identity<T>(arg: T): T {
  return arg
}

需要注意的是,泛型函数的返回值类型是根据你的业务需求决定,并非一定要返回泛型类型 T:

function identity<T>(arg: T): string {
  return String(arg)
}

代码解释:入参的类型是未知的,但是通过 String 转换,返回字符串类型。

多个类型参数

泛型函数可以定义多个类型参数:

function extend<T, U>(first: T, second: U): T & U {
  for(const key in second) {
    (first as T & U)[key] = second[key] as any
  }
  return first as T & U
}

代码解释: 这个函数用来合并两个对象,具体实现暂且不去管它,这里只需要关注泛型多个类型参数的使用方式,其语法为通过逗号分隔 <T, U, K>

泛型参数默认类型

函数参数可以定义默认值,泛型参数同样可以定义默认类型:

function min<T = number>(arr:T[]): T{
  let min = arr[0]
  arr.forEach((value)=>{
     if(value < min) {
         min = value
     }
  })
   return min
}
console.log(min([20, 6, 8n])) // 6

解释: 同样的不用去关注这个最小数函数的具体实现,要知道默认参数语法为 <T = 默认类型>

泛型类型与泛型接口

先来回顾下之前章节介绍的函数类型:

const add: (x: number, y: number) => string = function(x: number, y: number): string {
  return (x + y).toString()
}

等号左侧的 (x: number, y: number) => string 为函数类型。

再看下泛型类型:

function identity<T>(arg: T): T {
  return arg
}

let myIdentity: <T>(arg: T) => T = identity

同样的等号左侧的 <T>(arg: T) => T 即为泛型类型,它还有另一种带有调用签名的对象字面量书写方式:{ <T>(arg: T): T }:

function identity<T>(arg: T): T {
  return arg
}

let myIdentity: { <T>(arg: T): T } = identity

这就引导我们去写第一个泛型接口了。把上面例子里的对象字面量拿出来作为一个接口:

interface GenericIdentityFn {
  <T>(arg: T): T
}

function identity<T>(arg: T): T {
  return arg
}

let myIdentity: GenericIdentityFn = identity

进一步,把泛型参数当作整个接口的一个参数,我们可以把泛型参数提前到接口名上。这样我们就能清楚的知道使用的具体是哪个泛型类型:

interface GenericIdentityFn<T> {
  (arg: T): T
}

function identity<T>(arg: T): T {
  return arg
}

let myIdentity: GenericIdentityFn<number> = identity

注意,在使用泛型接口时,需要传入一个类型参数来指定泛型类型。示例中传入了 number 类型,这就锁定了之后代码里使用的类型。

泛型类

始终要记得,使用泛型是因为可以复用不同类型的代码。下面用一个最小堆算法举例说明泛型类的使用:

class MinClass {
  public list: number[] = []
  add(num: number) {
    this.list.push(num)
  }
  min(): number {
    let minNum = this.list[0]
    for (let i = 0; i < this.list.length; i++) {
      if (minNum > this.list[i]) {
        minNum = this.list[i]
      }
    }
    return minNum
  }
}

代码解释: 示例中我们实现了一个查找 number 类型的最小堆类,但我们的最小堆还需要支持字符串类型,此时就需要泛型的帮助了:

// 类名后加上 <T>
class MinClass<T> {
  public list: T[] = []
  add(num: T) {
    this.list.push(num)
  }
  min(): T {
    let minNum = this.list[0]
    for (let i = 0; i < this.list.length; i++) {
      if (minNum > this.list[i]) {
        minNum = this.list[i]
      }
    }
    return minNum
  }
}


let m = new MinClass<string>()
m.add('hello')
m.add('world')
m.add('generic')
console.log(m.min()) // generic

代码解释:

第 2 行,在声明 类 MinClass 的后面后加上了 <T>,这样就声明了泛型参数 T,作为一个变量可以是字符串类型,也可以是数字类型。

泛型约束

语法:通过 extends 关键字来实现泛型约束。

如果我们很明确传入的泛型参数是什么类型,或者明确想要操作的某类型的值具有什么属性,那么就需要对泛型进行约束。通过两个例子来说明:

interface User {
  username: string
}

function info<T extends User>(user: T): string {
  return 'imooc ' + user.username
}

代码解释: 示例中,第 5 行,我们约束了入参 user 必须包含 username 属性,否则在编译阶段就会报错。

下面再看另外一个例子:

type Args = number | string

class MinClass<T extends Args> {}

const m = new MinClass<boolean>() // Error, 必须是 number | string 类型

代码解释:

第 3 行,约束了泛型参数 T 继承自类型 Args,而类型 Args 是一个由 number 和 string 组成的联合类型。

第 5 行,泛型参数只能是 number 和 string 中的一种,传入 boolean 类型是错误的。

多重类型泛型约束

通过 <T extends Interface1 & Interface2> 这种语法来实现多重类型的泛型约束:

interface Sentence {
  title: string,
  content: string
}

interface Music {
  url: string
}

class Classic<T extends Sentence & Music> {
  private prop: T

  constructor(arg: T) {
    this.prop = arg
  }

  info() {
    return {
      url: this.prop.url,
      title: this.prop.title,
      content: this.prop.content
    }
  }
}

代码解释:

第 10 行,约束了泛型参数 T 需继承自交叉类型(后续有单节介绍) Sentence & Music,这样就能访问两个接口类型的参数。

2.类型的兼容性

前面小节中,介绍了 TypeScript 类型检查机制中的 类型推断类型保护,本节来介绍 类型兼容性

**定义:**类型兼容性用于确定一个类型是否能赋值给其他类型。

**结构化:**TypeScript 类型兼容性是基于结构类型的;结构类型只使用其成员来描述类型。

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

interface User {
  name: string,
  year: number
}

let protagonist = {
  name: 'Sherlock·Holmes',
  year: 1854,
  address: 'Baker Street 221B'
}

let user: User = protagonist // OK
let user2:User = {
  name: 'Sherlock·Holmes',
  year: 1854,
  address: 'Baker Street 221B'  //error,神奇的事情发生了,这样子直接将user2赋值一个对象,而这个对象包含接口不存在的属性,就会报错,而上面那种将user赋予给变量protagonist,这种形式是兼容的!
}

代码解释: 接口 User 中的每一个属性在 protagonist 对象中都能找到对应的属性,且类型匹配。另外,可以看到 protagonist 具有一个额外的属性 address,但是赋值同样会成功。

**函数的兼容性: ** 判断两个函数是否兼容,首先要看参数是否兼容,第二个还要看返回值是否兼容。

1.函数参数

先看一段代码示例:

let fn1 = (a: number, b: string) => {}
let fn2 = (c: number, d: string, e: boolean) => {}

fn2 = fn1 // OK
fn1 = fn2 // Error

代码解释:

第 4 行,将 fn1 赋值给 fn2 成立是因为:

  1. fn1 的每个参数均能在 fn2 中找到对应类型的参数
  2. 参数顺序保持一致,参数类型对应
  3. 参数名称不需要相同

第 5 行,将 fn2 赋值给 fn1 不成立,是因为 fn2 中的必须参数必须在 fn1 中找到对应的参数,显然第三个布尔类型的参数在 fn1 中未找到。

参数类型对应即可,不需要完全相同:

let fn1 = (a: number | string, b: string) => {}
let fn2 = (c: number, d: string, e: boolean) => {}

fn2 = fn1 // OK

代码解释: fn1 的第一个参数是 number 和 string 的联合类型,可以对应 fn2 的第一个参数类型 number,所以第 4 行赋值正常。

函数返回值

创建两个仅是返回值类型不同的函数:

let x = () => ({name: 'Alice'})
let y = () => ({name: 'Alice', location: 'Seattle'})

x = y // OK
y = x // Error

代码解释: 最后一行,函数 x() 缺少 location 属性,所以报错。

类型系统强制源函数的返回值类型必须是目标函数返回值类型的子类型。由此可以得出如果目标函数的返回值类型是 void,那么源函数返回值可以是任意类型:

let x : () => void
let y = () => 'imooc'

x = y // OK

枚举的类型兼容性

枚举与数字类型相互兼容:

enum Status {
  Pending,
  Resolved,
  Rejected
}

let current = Status.Pending
let num = 0

current = num
num = current

不同枚举类型之间是不兼容的:

enum Status { Pending, Resolved, Rejected }
enum Color { Red, Blue, Green }

let current = Status.Pending
current = Color.Red // Error

类的类型兼容性

类与对象字面量和接口的兼容性非常类似,但是类分实例部分和静态部分。

比较两个类类型数据时,只有实例成员会被比较,静态成员和构造函数不会比较。

class Animal {
  feet!: number
  constructor(name: string, numFeet: number) { }
}

class Size {
  feet!: number
  constructor(numFeet: number) { }
}

let a: Animal
let s: Size

a = s!  // OK
s = a  // OK

代码解释: 类 Animal 和类 Size 有相同的实例成员 feat 属性,且类型相同,构造函数参数虽然不同,但构造函数不参与两个类类型比较,所以最后两行可以相互赋值。

类的私有成员和受保护成员会影响兼容性。 允许子类赋值给父类,但是不能赋值给其它有同样类型的类。

class Animal {
  protected feet!: number
  constructor(name: string, numFeet: number) { }
}

class Dog extends Animal {}

let a: Animal
let d: Dog

a = d! // OK
d = a // OK

class Size {
  feet!: number
  constructor(numFeet: number) { }
}

let s: Size

a = s! // Error

代码解释:

第 13 行,子类可以赋值给父类。

第 14 行,父类之所以能够给赋值给子类,是因为子类中没有成员。

最后一行,因为类 Animal 中的成员 feet 是受保护的,所以不能赋值成功。

泛型的类型兼容性

泛型的类型兼容性根据其是否被成员使用而不同。先看一段代码示例:

interface Empty<T> {}

let x: Empty<number>
let y: Empty<string>

x = y! // OK

上面代码里,x 和 y 是兼容的,因为它们的结构使用类型参数时并没有什么不同。但是当泛型被成员使用时:

interface NotEmpty<T> {
  data: T
}
let x: NotEmpty<number>
let y: NotEmpty<string>

x = y! // Error

代码解释: 因为第 4 行,泛型参数是 number 类型,第 5 行,泛型参数是 string 类型,所以最后一行赋值失败。

如果没有指定泛型类型的泛型参数,会把所有泛型参数当成 any 类型比较:

let identity = function<T>(x: T): void {
  // ...
}

let reverse = function<U>(y: U): void {
  // ...
}

identity = reverse // OK

3.交叉类型

**定义:**交叉类型是将多个类型合并为一个类型。

这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。

语法为:类型一 & 类型二

interface Admin {
  id: number,
  administrator: string,
  timestamp: string
}

interface User {
  id: number,
  groups: number[],
  createLog: (id: number) => void,
  timestamp: number
}

let t: Admin & User

t!.administrator // 合法 Admin.administrator: string
t!.groups        // 合法 User.groups: number[]
t!.id            // 合法 id: number
t!.timestamp     // 合法 timestamp: never

4.联合类型

**定义:**联合类型与交叉类型很有关联,但是使用上却完全不同。区别在于:联合类型表示取值为多种中的一种类型,而交叉类型每次都是多个类型的合并类型。联合类型表示它可能是这些类型中的其中一个。

语法为:类型一 | 类型二

联合类型之间使用竖线 “|” 分隔:

let currentMonth: string | number

currentMonth = 'February'
currentMonth = 2

5.类型别名

**定义:**类型别名会给类型起个新名字。类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。

用关键字 type 定义类型别名。

类型别名不会新建一个类型,而是创建一个新名字来引用此类型

先看下面几个例子,

原始类型:

type brand = string
type used = true | false

const str: brand = 'imooc'
const state: used = true

联合类型:

type month = string | number

const currentMonth: month = 'February'
const nextMonth: month = 3

交叉类型:

interface Admin {
  id: number,
  administrator: string,
  timestamp: string
}

interface User {
  id: number,
  groups: number[],
  createLog: (id: number) => void,
  timestamp: number
}

type T = Admin & User

同接口一样,类型别名也可以是泛型:

type Tree<T, U> = {
  left: T,
  right: U
}

接口 vs 类型别名

类型别名看起来和接口非常类似,区别之处在于:

  • 接口可以实现 extends 和 implements,类型别名不行。
  • 类型别名并不会创建新类型,是对原有类型的引用,而接口会定义一个新类型。
  • 接口只能用于定义对象类型,而类型别名的声明方式除了对象之外还可以定义交叉、联合、原始类型等。

6.索引类型

**定义:**索引类型可以让 TypeScript 编译器覆盖检测到使用了动态属性名的代码。

要理解抽象的索引类型,需要先理解索引类型查询操作符(keyof)和索引访问操作符(T[K])。

索引类型查询操作符 - keyof

keyof 可以获取对象的可访问索引字符串字面量类型。

interface User {
  id: number,
  phone: string,
  nickname: string,
  readonly department: string,
}

class Token{
  private secret: string | undefined
  public accessExp: number = 60 * 60
  public refreshExp: number = 60 * 60 * 24 * 30 * 3
}

let user: keyof User // let user: "id" | "phone" | "nickname" | "department"
type token = keyof Token // type token = "accessExp" | "refreshExp"

代码解释:

倒数第二行,通过 let user: keyof User 得到了等价的 let user: "id" | "phone" | "nickname" | "department"

最后一行,通过 type token = keyof Token 得到了等价的 type token = "accessExp" | "refreshExp",注意这里没有 secret

可以看到对于任何类型 Tkeyof T 的结果为 T 上已知的公共属性名的联合

索引访问操作符 - T[K]

通过 keyof 拿到了属性名,接下来还要拿到属性名对应属性值的类型。

还是以上面的 Token 类为例:

class Token{
  public secret: string = 'ixeFoe3x.2doa'
  public accessExp: number = 60 * 60
  public refreshExp: number = 60 * 60 * 24 * 30 * 3
}

type token = keyof Token
type valueType = Token[token] // type valueType = string | number
type secret = Token['secret'] // type secret = string

代码解释:

通过 Token['secret'] 拿到了属性 secret 的类型为 string。

那么这时,我们知道了一个对象的类型为泛型 T,这个对象的属性类型 K 只需要满足 K extends keyof T,即可得到这个属性值的类型为 T[K]

理解了上面这段话,即可定义下面这个函数:

function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
  return o[name]; // o[name] is of type T[K]
}

代码解释: 已知参数 o 的类型为 T,参数 name 的类型 K 满足 K extends keyof T,那么返回值的类型即为 T[K]

7.映射类型

**定义:**映射类型可以将已知类型的每个属性都变为可选的或者只读的。

Readonly 与 Partial 关键字

先来看这样一个任务:将 Person 接口的每个属性都变为可选属性或者只读属性。

interface Person{
  name: string
  age: number
}

type PersonOptional = Partial<Person>
type PersonReadonly = Readonly<Person>

代码解释:

第 6 行,通过 Partial<Person> 这样的语法格式得到类型别名 PersonOptional,等价于:

type PersonOptional = {
  name?: string
  age?: number
}

第 7 行,通过 Readonly<Person> 这样的语法格式得到类型别名 PersonReadonly,等价于:

type PersonReadonly = {
  readonly name: string
  readonly age: number
}

两个关键字的源码分析

来看它们的实现源码:

type Readonly<T> = {
  readonly [K in keyof T]: T[K]
}
type Partial<T> = {
  [K in keyof T]?: T[K]
}

源码就使用了映射类型的语法 [K in Keys],来看这个语法的两个部分:

  1. 类型变量 K:它会依次绑定到每个属性,对应每个属性名的类型。
  2. 字符串字面量构成的联合类型的 Keys:它包含了要迭代的属性名的集合。

我们可以使用 for...in 来理解,它可以遍历目标对象的属性。

接下来继续分析:

  • Keys,可以通过 keyof 关键字取得,假设传入的类型是泛型 T,得到 keyof T,即为字符串字面量构成的联合类型("name" | "age")。
  • [K in keyof T],将属性名一一映射出来。
  • T[K],得到属性值的类型。

已知了这些信息,我们就得到了将一个对象所有属性变为可选属性的方法:

[K in keyof T]?: T[K]

进而可得:

type Partial<T> = {
  [K in keyof T]?: T[K]
}

Readonly<T>Partial<T> 都有着广泛的用途,因此它们与 Pick 一同被包含进了 TypeScript 的标准库里:

type Pick<T, K extends keyof T> = {
  [P in K]: T[P]
}

interface User {
  id: number
  age: number
  name: string
}

type PickUser = Pick<User, 'id'>

代码解释:

最后一行,就相当于 type PickUser = { id: number }

8.条件类型

**定义:**条件类型用来表达非均匀类型映射,可以根据一个条件表达式来进行类型检测,从两个类型中选出其中一个:

T extends U ? X : Y

语义类似三目运算符:若 TU 的子类型,则类型为 X,否则类型为 Y。若无法确定 T 是否为 U 的子类型,则类型为 X | Y

示例

declare function f<T extends boolean>(x: T): T extends true ? string : number

const x = f(Math.random() < 0.5) // const x: string | number

const y = f(true) // const y: string
const z = f(false) // const z: number

代码解释:

第 3 行,可以看到在条件不确定的情况下,得到了联合类型 string | number

最后两行,条件确定时,得到了具体类型 stringnumber

可分配条件类型

在条件类型 T extends U ? X : Y 中,当泛型参数 T 取值为 A | B | C 时,这个条件类型就等价于 (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y),这就是可分配条件类型。

可分配条件类型(distributive conditional type)中被检查的类型必须是裸类型参数(naked type parameter)。裸类型表示没有被包裹(Wrapped) 的类型,(如:Array<T>[T]Promise<T> 等都不是裸类型),简而言之裸类型就是未经过任何其他类型修饰或包装的类型。

定义:is 关键字一般用于函数返回值类型中,判断参数是否属于某一类型,并根据结果返回对应的布尔类型。

语法:prop is type

在一些兑换码场景,经常会需要将兑换码全部转为大写,之后再进行判断:

function isString(s: unknown): boolean {
  return typeof s === 'string'
}

function toUpperCase(x: unknown) {
  if(isString(x)) {
    x.toUpperCase() // Error, Object is of type 'unknown'
  }
}

代码解释:

第 7 行,可以看到 TypeScript 抛出了一个错误提示,一个 unknown 类型的对象不能进行 toUpperCase() 操作,可是在上一行明明已经通过 isString() 函数确认参数 x 为 string 类型,但是由于函数嵌套 TypeScript 不能进行正确的类型判断。

这时,就可以使用 is 关键字:

const isString = (s: unknown): s is string => typeof s === 'string'

function toUpperCase(x: unknown) {
  if(isString(x)) {
    x.toUpperCase()
  }
}

解释: 通过 is 关键字将类型范围缩小为 string 类型,这也是一种代码健壮性的约束规范。

9.is 关键字

定义:is 关键字一般用于函数返回值类型中,判断参数是否属于某一类型,并根据结果返回对应的布尔类型。

语法:prop is type

在一些兑换码场景,经常会需要将兑换码全部转为大写,之后再进行判断:

function isString(s: unknown): boolean {
  return typeof s === 'string'
}

function toUpperCase(x: unknown) {
  if(isString(x)) {
    x.toUpperCase() // Error, Object is of type 'unknown'
  }
}

代码解释:

第 7 行,可以看到 TypeScript 抛出了一个错误提示,一个 unknown 类型的对象不能进行 toUpperCase() 操作,可是在上一行明明已经通过 isString() 函数确认参数 x 为 string 类型,但是由于函数嵌套 TypeScript 不能进行正确的类型判断。

这时,就可以使用 is 关键字:

const isString = (s: unknown): s is string => typeof val === 'string'

function toUpperCase(x: unknown) {
  if(isString(x)) {
    x.toUpperCase()
  }
}

解释: 通过 is 关键字将类型范围缩小为 string 类型,这也是一种代码健壮性的约束规范。

10.infer 关键字

**定义:**在条件类型表达式中,可以在 extends 条件语句中使用 infer 关键字来声明一个待推断的类型变量。

infer理解起来比较抽象,可以通俗的讲,我们来通过一个类比来帮助理解。语句 let num 中,通过 let 来声明了一个变量,那怎样声明一个不确定的类型变量呢? 答案是使用 infer 关键字,infer R 就是声明了一个类型变量 R

通过 ReturnType 理解 infer

infer 相对比较难理解,我们先看下 TypeScript 一个内置工具类型 ReturnType

  • ReturnType<T> – 获取函数返回值类型。
const add = (x:number, y:number) => x + y
type t = ReturnType<typeof add> // type t = number

代码解释:

通过 ReturnType 可以得到函数 add() 的返回值类型为 number 类型。但要注意不要滥用这个工具类型,应尽量多的手动标注函数返回值类型。

来看一下 ReturnType 的实现源码:

/**
 * Obtain the return type of a function type
 */
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any

infer 的作用是让 TypeScript 自己推断,并将推断的结果存储到一个类型变量中,infer 只能用于 extends 语句中。

再来看 ReturnType 的实现:如果 T 满足约束条件 (...args: any) => any,并且能够赋值给 (...args: any) => infer R,则返回类型为 R,否则为 any 类型。

继续看几个例子:

type T0 = ReturnType<() => string>        // string
type T1 = ReturnType<(s: string) => void> // void
type T2 = ReturnType<<T>() => T>          // unknown

代码解释:

分别可以得到 type T0 = string type T1 = void type T2 = unknown,只要满足约束条件 (...args: any) => any,TypeScript 推断出函数的返回值,并借助 infer 关键字将其储存在类型变量 R 中,那么最终得到返回类型 R

借助 infer 实现元组转联合类型

借助 infer 可以实现元组转联合类型,如:[string, number] -> string | number

type Flatten<T> = T extends Array<infer U> ? U : never

type T0 = [string, number]
type T1 = Flatten<T0> // string | number

代码解释:

第 1 行,如果泛型参数 T 满足约束条件 Array<infer U>,那么就返回这个类型变量 U

第 3 行,元组类型在一定条件下,是可以赋值给数组类型,满足条件:

type TypeTuple = [string, number] 
type TypeArray = Array<string | number>

type B0 = TypeTuple extends TypeArray ? true : false // true

第 4 行,就可以得到 type T1 = string | number

11.Truthy 与 Falsy

Truthy 指的是转换后的值为’真‘的值,Falsy 是在 Boolean 上下文中已认定可转换为‘假‘的值。

变量类型 Truthy Falsy
boolean true false
string 非空字符串 ‘’
number 其他数字 0 / NaN
null 总是为假
undefined 总是为假
object 总是为真,包括 {}、[]、() => {}

需要注意下,空函数、空数组、空对象这些都是 Truthy,返回 true。

! 与 !!

操作符 ! 表示取反,得到一个布尔值:

let fn = () => {}
let obj = {}
let arr: never[] = []

console.log(!fn)  // false
console.log(!obj) // false
console.log(!arr) // false

let num = 10
let str = 'imooc'

console.log(!num) // false
console.log(!str) // false

let n = null
let u = undefined
let N = NaN
let z = 0

console.log(!n)   // true
console.log(!u)   // true
console.log(!N)   // true
console.log(!z)   // true

解释: 前三行的变量都是 truthy,为真,取反则得到 false。

操作符 !! 表示变量被强制类型转换为布尔值后的值:

let fn = () => {}
let obj = {}
let arr: never[] = []

console.log(!!fn)  // true
console.log(!!obj) // true
console.log(!!arr) // true

let num = 10
let str = 'imooc'

console.log(!!num) // true
console.log(!!str) // true

let n = null
let u = undefined
let N = NaN
let z = 0

console.log(!!n)   // false
console.log(!!u)   // false
console.log(!!N)   // false
console.log(!!z)   // false

12.混入(Mixins)

**定义:**在 TypeScript 中,可以根据不同的功能定义多个可复用的类,它们将作为 mixins。因为 extends 只支持继承一个父类,我们可以通过 implements 来连接多个 mixins,并且使用原型链连接子类的方法和父类的方法。

这就像组件拼合一样,由一堆细粒度的 mixins 快速搭建起一个功能强大的类。

简单的对象混入

先来看一个基础例子:

let target = {  a: 1,  b: 1 }
let source1 = {  a: 2,  c: 3 }
let source2 = {  b: 2,  d: 4 }

Object.assign(target, source1, source2)

console.log(target) // { a: 2, b: 2, c: 3, d: 4 }

解释: 通过 Object.assign()source1source2 混入到 target 上,并且替换了 target 对象原有的属性值。

TypeScript Mixins

先介绍一个前置知识: Object.getOwnPropertyNames() 方法返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组。

代码演示

下面的代码演示了如何在 TypeScript 中使用混入:

// Disposable Mixin
class Disposable {
  isDisposed!: boolean
  dispose() {
    this.isDisposed = true
  }
}

// Activatable Mixin
class Activatable {
  isActive!: boolean;
  activate() {
    this.isActive = true
  }
  deactivate() {
    this.isActive = false
  }
}

class SmartObject{
  constructor() {
    setInterval(() => console.log(this.isActive + " : " + this.isDisposed), 500)
  }

  interact() {
    this.activate()
  }

  // Disposable
  isDisposed: boolean = false
  dispose!: () => void
  // Activatable
  isActive: boolean = false
  activate!: () => void
  deactivate!: () => void
}
applyMixins(SmartObject, [Disposable, Activatable])

let smartObj = new SmartObject()
setTimeout(() => smartObj.interact(), 2000)

function applyMixins(derivedCtor: any, baseCtors: any[]) {
  baseCtors.forEach(baseCtor => {
    Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
      derivedCtor.prototype[name] = baseCtor.prototype[name]
    })
  })
}

TypeScript工程

1.TypeScript 模块

模块在其自身的作用域里执行,而不是在全局作用域里。

export: 导出模块中的变量、函数、类、接口等;

import: 导入其他模块导出的变量、函数、类、接口等。

TypeScript 与 ECMAScript 2015 一样,任何包含顶级 import 或者 export 的文件都被当成一个模块。相反的,如果一个文件不带有顶级的 import 或者 export 声明,那么它的内容被视为全局可见的。

全局模块

在一个 TypeScript 工程创建一个 test.ts 文件,写入代码:

const a = 1

然后,在相同的工程下创建另一个 test2.ts 文件,写入代码:

const a = 2

此时编译器会提示重复定义错误,虽然是在不同的文件下,但处于同一全局空间。

如果加上 export 导出语句:

export const a = 1

这样,两个 a 因处于不同的命名空间,就不会报错。

导出语法

使用 export 导出声明

任何声明(比如变量,函数,类,类型别名或接口)都能够通过添加 export 关键字来导出。

export.ts:

export const a: number = 1
export const add = (x: number, y:number) => x + y 

export interface User {
  nickname: string,
  department: string
}
export class Employee implements User {
  public nickname!: string
  public department!: string
}

export type used = true | false

解释: 每个声明都通过 export 关键字导出。

先声明,后导出

const a: number = 1
const add = (x: number, y:number) => x + y 

interface User {
  nickname: string,
  department: string
}
class Employee implements User {
  public nickname!: string
  public department!: string
}

type used = true | false

export { a, add, Employee }

解释: 先进行声明操作,最终统一使用 export 关键字导出。

导出时重命名

const a: number = 1
const add = (x: number, y:number) => x + y 

interface User {
  nickname: string,
  department: string
}
class Employee implements User {
  public nickname!: string
  public department!: string
}

type used = true | false

export { add }
export { a as level, used as status, Employee }

解释: 在导出时,可以用 as 关键字将声明重命名。

重新导出

重新导出功能并不会在当前模块导入那个模块或定义一个新的局部变量。

ZipCodeValidator.ts:

export interface StringValidator {
  isAcceptable(s: string): boolean
}

export const numberRegexp = /^[0-9]+$/

class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
  return s.length === 5 && numberRegexp.test(s)
}
}
export { ZipCodeValidator }
export { ZipCodeValidator as mainValidator }

ParseIntBasedZipCodeValidator.ts:

export class ParseIntBasedZipCodeValidator {
  isAcceptable(s: string) {
    return s.length === 5 && parseInt(s).toString() === s
  }
}

// 导出原先的验证器但做了重命名
export {ZipCodeValidator as RegExpBasedZipCodeValidator} from './ZipCodeValidator'

代码解释: 在 ParseIntBasedZipCodeValidator.ts 文件中,重新导出 ZipCodeValidator.ts 文件中的声明。

或者一个模块可以包裹多个模块,并把他们导出的内容联合在一起通过语法:export * from 'module'

比如在 validator.ts 文件中,统一导出这两个模块。

// validator.ts
export * from './ZipCodeValidator'
export * from './ParseIntBasedZipCodeValidator'

默认导出

export default class ZipCodeValidator {
  static numberRegexp = /^[0-9]+$/
  isAcceptable(s: string) {
    return s.length === 5 && ZipCodeValidator.numberRegexp.test(s)
  }
}

代码解释: 每个模块都可以有一个 default 导出,且一个模块只能够有一个 default 导出。

导入语法

1.使用 import 导入

使用 import 形式来导入其它模块中的导出内容。

import { a, add, Employee } from './export'

2.导入时重命名

import { a as level, used as status } from './export'

3.将整个模块导入到一个变量

将整个模块导入到一个变量,并通过它来访问模块的导出部分

import * as TYPES from './export'

4.直接导入

import './export'

export =import = require()

CommonJS 和 AMD 的环境里都有一个 exports 变量,这个变量包含了一个模块的所有导出内容。

CommonJS 和 AMD 的 exports 都可以被赋值为一个 对象, 这种情况下其作用就类似于 EcmaScript 2015 语法里的默认导出,即 export default 语法了。虽然作用相似,但是 export default 语法并不能兼容 CommonJS 和 AMD 的 exports。

为了支持 CommonJS 和 AMD 的 exports, TypeScript 提供了 export = 语法。

export = 语法定义一个模块的导出 对象。 这里的 对象 一词指的是类,接口,命名空间,函数或枚举

若使用 export = 导出一个模块,则必须使用 TypeScript 的特定语法 import module = require('module') 来导入此模块。

  • export = 只能导出 对象
  • export = 导出的模块只能用 import = require() 形式导入

文件 ZipCodeValidator.ts:

let numberRegexp = /^[0-9]+$/
class ZipCodeValidator {
  isAcceptable(s: string) {
    return s.length === 5 && numberRegexp.test(s)
  }
}
export = ZipCodeValidator

代码解释: 使用 export = 语法导出一个类对象。

文件 Test.ts:

import Zip = require('./ZipCodeValidator')

// Some samples to try
let strings = ['Hello', '98052', '101']

// Validators to use
let validator = new Zip()

// Show whether each string passed each validator
strings.forEach(s => {
  console.log(`'${ s }' - ${ validator.isAcceptable(s) ? 'matches' : 'does not match' }`)
});

代码解释: 通过 import = require() 形式导入。

2.命名空间

**定义:**使用 namespace 关键字来声明命名空间。

TypeScript 的命名空间可以将代码包裹起来,只对外暴露这个命名空间对象,通过 export 关键字将命名空间内的变量挂载到命名空间对象上。

命名空间的本质

命名空间本质上就是一个对象,将其内部的变量组织到这个对象的属性上:

namespace Calculate {
  const fn = (x: number, y: number) => x * y 
  export const add = (x: number, y:number) => x + y
}

来看其编译后的结果:

"use strict";
var Calculate;
(function (Calculate) {
    var fn = function (x, y) { return x * y; };
    Calculate.add = function (x, y) { return x + y; };
})(Calculate || (Calculate = {}));

那么,我们就可以访问 Calculate 对象上的 add 属性了:

Calculate.add(2, 3)

命名空间主要是为解决全局作用域内重名问题,而这一问题随着模块化编程的使用,已经得到了解决。

3.声明合并

定义:TypeScript 编译器会将程序中多个具有相同名称的声明合并为一个声明。

但这并不是说 TypeScript 会随意的合并两个名称相同的字符串变量,这显然是不符合语法规定的,那么本节将介绍什么样的声明可以进行合并。

实体创建

TypeScript 中的声明会创建以下三种实体之一:命名空间、类型或值。

来看以下声明都创建了什么实体:

声明类型 创建了命名空间 创建了类型 创建了值
Namespace
Class
Enum
Interface
Type Alias
Function
Variable

合并接口

最简单也最常见的声明合并类型是接口合并。

interface Box {
  height: number
  width: number
}

interface Box {
  scale: number
  width: number // 类型相同 OK
}

let box: Box = {height: 5, width: 6, scale: 10}

接口合并,则接口的非函数的成员须是唯一的,哪怕不唯一,最起码也要类型相同。但如果类型不同,则编辑器报错。

对于函数成员,每个同名函数声明都会被当成这个函数的一个重载,后面的接口具有更高优先级。

接口合并时,将遵循以下规范:

  • 接口内优先级是从上到下;
  • 后面的接口具有更高优先级;
  • 如果函数的参数是字符串字面量,会被提升到函数声明的最顶端。
interface Document {
  createElement(tagName: any): Element              // 5
}
interface Document {
  createElement(tagName: 'div'): HTMLDivElement     // 2
  createElement(tagName: 'span'): HTMLSpanElement   // 3
}
interface Document {
  createElement(tagName: string): HTMLElement         // 4
  createElement(tagName: 'canvas'): HTMLCanvasElement // 1
}

按照上面介绍的规则,得到合并后的声明:

interface Document {
  createElement(tagName: 'canvas'): HTMLCanvasElement
  createElement(tagName: 'div'): HTMLDivElement
  createElement(tagName: 'span'): HTMLSpanElement
  createElement(tagName: string): HTMLElement
  createElement(tagName: any): Element
}

合并命名空间

合并多个具有相同名称的命名空间:

  • 导出成员不可重复定义
  • 非导出成员仅在其原有的(合并前的)命名空间内可见
namespace A {
  let used = true

  export function fn() {
      return used
  }
}

namespace A {
  export function fnOther() {
      return used // Error, 未找到变量 used
  }
}

A.fn()      // OK
A.fnOther() // OK

代码解释:

第一个命名空间内的非导出成员 used 仅在第一个命名空间内可见。 命名空间对象 A 可以分别访问在第一个或第二个声明的导出成员。

命名空间与其它类型的合并

只要命名空间的定义符合将要合并类型的定义,命名空间就可以与其它类型的声明进行合并,合并结果包含两者的声明类型。

命名空间与类的合并

合并名称相同的命名空间与类:

  • 命名空间内的成员必须导出,合并后的类才能访问
  • 命名空间内导出的成员,相当于合并后类的静态属性
  • 命名空间要放在类的定义后面
class Album {
  label!: Album.AlbumLabel
}
namespace Album {
  export class AlbumLabel { }
  export const num = 10
}

console.log(Album.num) // 10

注意: 命名空间要放在类的定义后面,命名空间内导出的成员,相当于合并后类的静态属性。

命名空间与函数的合并

  • 名称相同的命名空间与函数挂载同一个对象
  • 命名空间要放在函数的定义后面
function buildLabel(name: string): string {
  return buildLabel.prefix + name + buildLabel.suffix
}

namespace buildLabel {
  export let suffix = '.C'
  export let prefix = 'Hello, '
}

console.log(buildLabel('Mr.Pioneer')) // Hello, Mr.Pioneer.C

命名空间和函数可以进行合并,是因为在 JavaScript 中函数也是对象。

命名空间与枚举的合并

命名空间可以用来扩展枚举型:

enum Color {
  red = 1,
  green = 2,
  blue = 4
}

namespace Color {
  export function mixColor(colorName: string) {
    switch (colorName) {
      case 'yellow':
        return Color.red + Color.green
      case 'white':
        return Color.red + Color.green + Color.blue
      default:
        break
    }
  }
}

console.log(Color.mixColor('yellow')) // 3

解释: 枚举本身也是个对象,与命名空间对象合并后对象的属性进行了扩充。

4.编译选项

TypeScript 提供了很多不同功能的编译选项,既可以通过配置 tsconfig.json 文件中的 compilerOptions 属性来实现编译,也可以使用在 tsc 命令后跟随参数这形式,直接编译 .ts 文件。

注意: 当命令行上指定了输入文件时,tsconfig.json 文件会被忽略。

编译选项

选项 类型 默认值 描述
–allowJs boolean false 允许编译 JavaScript 文件
–allowSyntheticDefaultImports boolean false 允许从没有设置默认导出的模块中默认导入
–allowUnreachableCode boolean false 不报告执行不到的代码错误
–allowUnusedLabels boolean false 不报告未使用的标签错误
–alwaysStrict boolean false 以严格模式解析并为每个源文件生成 "use strict" 语句
--baseUrl string 解析非相对模块名的基准目录
–charset string “utf8” 输入文件的字符集
–checkJs boolean false .js 文件中报告错误,与 --allowJs 配合使用
–declaration -d boolean false 生成相应的 .d.ts 文件
–declarationDir string 生成声明文件的输出路径
–diagnostics boolean false 显示诊断信息
–disableSizeLimit boolean false 禁用 JavaScript 工程体积大小的限制
–emitBOM boolean false 在输出文件的开头加入BOM头(UTF-8 Byte Order Mark)
–emitDecoratorMetadata[1] boolean false 给源码里的装饰器声明加上设计类型元数据。查看 issue #2577 了解更多信息。
–experimentalDecorators[1] boolean false 启用实验性的ES装饰器
–extendedDiagnostics boolean false 显示详细的诊断信息
–forceConsistentCasingInFileNames boolean false 禁止对同一个文件的不一致的引用
–help -h 打印帮助信息
–importHelpers string tslib 导入辅助工具函数(比如 __extends__rest等)
–inlineSourceMap boolean false 生成单个 sourcemaps 文件,而不是将每 sourcemaps 生成不同的文件
–inlineSources boolean false 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap--sourceMap 属性
--init 初始化 TypeScript 项目并创建一个 tsconfig.json 文件
–isolatedModules boolean false 将每个文件作为单独的模块(与 “ts.transpileModule” 类似)
–jsx string “Preserve” .tsx 文件里支持 JSX: “React” 或 “Preserve”。
–jsxFactory string “React.createElement” 指定生成目标为 react JSX 时,使用的 JSX 工厂函数,比如 React.createElement 或 h
–lib string[] 编译过程中需要引入的库文件的列表。 可能的值为: ► ES5ES6ES2015ES7ES2016ES2017ES2018ESNextES5ES5ES5ES5ES5ES5DOMDOM.IterableWebWorkerScriptHostES2015.CoreES2015.CollectionES2015.GeneratorES2015.IterableES2015.PromiseES2015.ProxyES2015.ReflectES2015.SymbolES2015.Symbol.WellKnownES2016.Array.IncludeES2017.objectES2017.IntlES2017.SharedMemoryES2017.StringES2017.TypedArraysES2018.IntlES2018.PromiseES2018.RegExpESNext.AsyncIterableESNext.ArrayESNext.IntlESNext.Symbol 注意:如果 --lib 没有指定默认注入的库的列表。默认注入的库为: ► 针对于 --target ES5DOM,ES5,ScriptHost ► 针对于 --target ES6DOM,ES6,DOM.Iterable,ScriptHost
–listEmittedFiles boolean false 打印出编译后生成文件的名字
–listFiles boolean false 编译过程中打印文件名
–locale string (platform specific) 显示错误信息时使用的语言,比如:en-us
–mapRoot string 为调试器指定指定 sourcemap 文件的路径,而不是使用生成时的路径。当 .map 文件是在运行时指定的,并不同于 js 文件的地址时使用这个标记。指定的路径会嵌入到 sourceMap 里告诉调试器到哪里去找它们
–maxNodeModuleJsDepth number 0 node_modules 依赖的最大搜索深度并加载 JavaScript 文件,仅适用于 --allowJs
–module -m string target === “ES6” ? “ES6” : “commonjs” 指定生成哪个模块系统代码: “None”, “CommonJS”, “AMD”, “System”, “UMD”, "ES6"或 “ES2015”。 ► 只有 "AMD"和 "System"能和 --outFile一起使用。 ► "ES6"和 "ES2015"可使用在目标输出为 "ES5"或更低的情况下。
–moduleResolution string module === “AMD” or “System” or “ES6” ? “Classic” : “Node” 决定如何处理模块。或者是 “Node” 对于 Node.js/io.js,或者是 “Classic”(默认)
–newLine string (platform specific) 当生成文件时指定行结束符: "crlf"(windows)或 "lf"(unix)
–noEmit boolean false 不生成输出文件
–noEmitHelpers boolean false 不在输出文件中生成用户自定义的帮助函数代码,如 __extends
–noEmitOnError boolean false 报错时不生成输出文件
–noErrorTruncation boolean false 不截短错误消息
–noFallthroughCasesInSwitch boolean false 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)
--noImplicitAny boolean false 在表达式和声明上有隐含的 any 类型时报错
–noImplicitReturns boolean false 不是函数的所有返回路径都有返回值时报错
–noImplicitThis boolean false this 表达式的值为 any 类型的时候,生成一个错误
–noImplicitUseStrict boolean false 模块输出中不包含 “use strict” 指令
–noLib boolean false 不包含默认的库文件( lib.d.ts
–noResolve boolean false 不把 /// <reference``> 或模块导入的文件加到编译文件列表
–noStrictGenericChecks boolean false 禁用在函数类型里对泛型签名进行严格检查
–noUnusedLocals boolean false 若有未使用的局部变量则抛错
–noUnusedParameters boolean false 若有未使用的参数则抛错
--outDir string 重定向输出目录
–outFile string 将输出文件合并为一个文件,合并的顺序是根据传入编译器的文件顺序和 ///<reference``>import 的文件顺序决定的
–skipDefaultLibCheck boolean false 忽略库的默认声明文件的类型检查
–skipLibCheck boolean false 忽略所有的声明文件( *.d.ts )的类型检查
--sourceMap boolean false 生成相应的 .map 文件
–sourceRoot string 指定 TypeScript 源文件的路径,以便调试器定位。当 TypeScript 文件的位置是在运行时指定时使用此标记, 路径信息会被加到 sourceMap 里
--strict boolean false 启用所有严格类型检查选项
–strictFunctionTypes boolean false 禁用函数参数双向协变检查
–strictPropertyInitialization boolean false 确保类的非 undefined 属性已经在构造函数里初始化。若要令此选项生效,需要同时启用 --strictNullChecks
--strictNullChecks boolean false 在严格的 null 检查模式下,nullundefined 值不包含在任何类型里,只允许用它们自己和 any 来赋值(有个例外, undefined 可以赋值到 void)
–stripInternal[1] boolean false 不对具有 /** @internal */ JSDoc注解的代码生成代码
–suppressExcessPropertyErrors[1] boolean false 阻止对对象字面量的额外属性检查
–suppressImplicitAnyIndexErrors boolean false 阻止 --noImplicitAny 对缺少索引签名的索引对象报错。查看 issue #1232 了解详情。
--target <br> -t string “ES3” 指定ECMAScript目标版本 "ES3"(默认), "ES5""ES6"/ "ES2015""ES2016""ES2017""ESNext"。 注意: "ESNext" 最新的生成目标列表为 ES proposed features
–traceResolution boolean false 生成模块解析日志信息
–types string[] 要包含的类型声明文件名列表
–typeRoots string[] 要包含的类型声明文件路径列表
--version <br> -v 打印编译器版本号
–watch -w 在监视模式下运行编译器。会监视输出文件,在它们改变时重新编译。监视文件和目录的具体实现可以通过环境变量进行配置
  • [1] 这些选项是试验性的

5.tsconfig.json 配置

本节将详细介绍 tsconfig.json 文件中各配置项的含义,这将对我们搭建一个 TypeScript 工程项目很有帮助。

如果一个目录下存在一个 tsconfig.json 文件,那么它意味着这个目录是 TypeScript 项目的根目录,tsconfig.json 文件中指定了用来编译这个项目的根文件和编译选项。

一个项目可以通过以下方式之一来编译:

  • 不带任何输入文件的情况下调用 tsc 命令,编译器会从当前目录开始去查找 tsconfig.json 文件,逐级向上搜索父目录。
  • 不带任何输入文件的情况下调用 tsc 命令,且使用命令行参数 --project(或 -p )指定一个包含 tsconfig.json 文件的目录。

当命令行上指定了输入文件时,tsconfig.json 文件会被忽略。