本文是vue3+ts项目系列第2篇《TypeScript 语法汇总》,关于TypeScript的基础语法,你了解…

一、背景

在 react 和 vue 社区中也越来越多人开始使用TypeScript,使用 TS 可以增加代码的可读性和可维护性。从发布的 Vue3 正式版本来看, Vue3 的源码是用 TS 编写的,更好的 TypeScript 支持也是这次升级的一大亮点。当然,在实际开发中如何正确拥抱 TS 以及如何迁移到 Vue3 也是项目中我们不得不面对的问题,这里针对 Vue3 和 TS单独做了一个系列和大家做一下交流,本篇是 vue3+ts项目系列第2篇《TypeScript 语法汇总》。

本系列其他内容如下:

二、前言

1、静态类型、动态类型、强类型、弱类型

编译时就知道变量类型的是静态类型,运行时才知道一个变量类型的叫做动态类型。 java 是静态类型, js 是动态类型。

不允许隐式转换的是强类型,允许隐式转换的是弱类型。 java 是强类型, js 是弱类型。

那ts到底是什么类型的语言,很明显, ts 是静态类型语言,因为它需要经过编译。但是 ts不是强类型,因为它可以允许隐式类型转换。

let isBool: boolean
let num: number = 10
isBool = !num // ok

2、Typescript是什么

  • ECMAScript 的超集 (stage 3)
  • 编译期的类型检查
  • 不引入额外开销(零依赖,不扩展 js 语法,不侵入运行时)
  • 编译出通用的、易读的 js 代码

Typescript = Type + ECMAScript + Babel-Lite

Typescript 设计目标: 链接

3、为什么使用 Typescript

  • 增加了代码的可读性和可维护性
  • 减少运行时错误,写出的代码更加安全,减少 BUG
  • 享受到代码提示带来的快感
  • 重构神器

三、类型

1、基础类型

声明了变量的类型,那么这个变量就有了静态类型的特性,ts中使用:操作符来声明类型:

  • boolean
  • number
  • string
  • array
  • tuple(元组)
  • enum(枚举)
  • any & unknown
  • void
  • null & undefined
  • never
  • Object

声明变量的类型:

let bool: boolean = false // boolean 类型
let num: number = 12 // number 类型
let str: string = 'hello world' // string类型
let numArr: number[] = [1, 2, 3] // number数组类型
let numArr: Array<number> = [1, 2, 3] // 数组泛型形式:number数组类型

let unknow: any = 4 // any类型,即任一类型
unknow = "maybe a string instead"
unknow = false

1.1 枚举(enum

  • 数字枚举
  • 字符串枚举
  • 常量枚举

很多编程语言都有枚举的概念,枚举就是一组常量的集合,但是和集合不同,枚举可以通过变量的值来得到变量,它是一个双向的过程:

enum Direction {
  UP,
  DOWN,
  LEFT,
  RIGHT
}

Direction.UP // 0
Direction[0] // 'UP'

数字枚举

上例中的 Direction 就是一个数字枚举,默认的,第一个变量的值是0,后面的值会在前一个值上 +1 ,所以 DOWN 的值为1, LEFT 的值为2,以此类推。 如果想改变枚举的初始值,只需要给第一个变量赋值即可:

enum Direction {
  UP = 1,
  DOWN,
  LEFT,
  RIGHT
}

字符串枚举
字符串枚举的概念很简单, 在一个字符串枚举里,每个成员都必须初始化:

如果某个成员没有被初始化,则会报错

enum Direction {
  UP = 'UP',
  DOWN = 'DOWN',
  LEFT = 'LEFT',
  RIGHT = 'RIGHT',
  MIDDLE //error 枚举成员必须具有初始化表达式
}

常量枚举
可以用 const 修饰符来声明枚举,这时候编译后的js代码将不会出现额外的声明代码:

enum Direction {
  UP,
  DOWN,
  LEFT,
  RIGHT
}
const enum Seasons {
  SPRING = 'SPRING',
  SUMMER = 'SUMMER',
  AUTUMN = 'AUTUMN',
  WINTER = 'WINTER'
}

const up = Direction.UP
const spring = Seasons.SPRING

生成的js:

var Direction;
(function (Direction) {
  Direction[Direction["UP"] = 0] = "UP";
  Direction[Direction["DOWN"] = 1] = "DOWN";
  Direction[Direction["LEFT"] = 2] = "LEFT";
  Direction[Direction["RIGHT"] = 3] = "RIGHT";
})(Direction || (Direction = {}));

var up = Direction.UP;
var spring = "SPRING" /* SPRING */;

1.2 anyunknown 的区别

  • any : 任意类型
  • unknown : 未知的类型

任何类型都能分配给 unknown ,但 unknown 不能分配给其他基本类型,而 any 啥都能分配和被分配

unknown Demo:

let foo: unknown
foo = true // ok
foo = 123 //ok

foo.toFixed(2) // error

let foo1: string = foo // error

any Demo:

let bar: any
bar = true // ok
bar = 123 //ok

foo.toFixed(2) // ok

let bar1:string = bar // ok

如上,用了 any 就相当于完全丢失了类型检查,所以尽量少用 any ,对于未知类型可以用 unknown

unknown 的正确用法
可以通过不同的方式将 unknown 类型缩小为更具体的类型范围:

function getLen(value: unknown): number {
  if (typeof value === 'string') {
    // 因为类型保护的原因,此处 value 被判断为 string 类型
    return value.length
  }
  
  return 0
}

这个过程叫类型收窄(type narrowing)

1.3 never

  • never 类型表示的是那些永不存在的值的类型。

never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型;变量也可能是 never类型,当它们被永不为真的类型保护所约束时。

  • never 类型是任何类型的子类型,也可以赋值给任何类型

没有类型是never的子类型或可以赋值给never类型(除了never本身之外),即使 any也不可以赋值给never

在最新的 typescript 3.7 中,下面代码会报错:

// never 用户控制流分析
function neverReach (): never {
  throw new Error('an error')
}

const x = neverReach()
x.toFixed(2) // x is unreachable

never 还可以用于联合类型的幺元:

type T0 = string | number | never // T0 is string | number

2、高级类型

2.1 联合类型与交叉类型

联合类型(union type)表示多种类型的 “或” 关系

function genLen(x: string | any[]) {
  return x.length
}

genLen('') // ok
genLen([]) // ok
genLen(1) // error

交叉类型表示多种类型的 “与” 关系

interface Person {
  name: string
  age: number
}

interface Animal {
  name: string
  color: string
}

const x: Person & Animal = {
  name: 'x',
  age: 1,
  color: 'red
}

使用联合类型表示枚举

type Position = 'UP' | 'DOWN' | 'LEFT' | 'RIGHT'
const position: Position = 'UP'
// 具值联合类型
// type 关键词可以声明一个类型
type positionType = 'top' | 'center' | 'bottom'
let position: positionType = 'top' // ok
position = 'left' // error 不能将类型“"left"”分配给类型“positionType”
position = 'bottom' // ok

// 数组联合类型
let arr: (number | string)[] = []
arr.push(1) // ok
arr.push('hello') // ok
arr.push(true) // error 类型“true”的参数不能赋给类型“string | number”的参数。

可以避免使用 enum 侵入了运行时。

2.2 类型保护和类型断言

ts 初学者很容易写出下面的代码:

function isString (value) {
  return Object.prototype.toString.call(value) === '[object String]'
}

function fn (x: string | number) {
  if (isString(x)) {
    return x.length // error 类型“string | number”上不存在属性“length”。
  } else {
    // .....
  }
}

如何让 ts 推断出来 x 是 string 类型呢?

2.2.1 使用ts的 is 关键词
function isString (value: unknown): value is string {
  return Object.prototype.toString.call(value) === '[object String]'
}

function fn (x: string | number) {
  if (isString(x)) {
    return x.length
  } else {
    // .....
  }
}
2.2.2 类型保护
(1) js typeof 关键词

typeof 关系词可以从实现 推出类型,这是一个类型关键词

function getLen(x: string | any[]) {
  return x.length
}

type GetLen = typeof getLen

function callback(fn: GetLen) {
  fn(1) // 类型“1”的参数不能赋给类型“string | any[]”的参数
}
(2) js instanceof 关键词

在 ts 中,instanceof 关键词能够帮助 ts 判断出构造函数的类型:

function fn1 (x: XMLHttpRequest | string) {
  if (x instanceof XMLHttpRequest) { // x is XMLHttpRequest
    return x.getAllResponseHeaders()
  } else { // x is string
    return x.length
  }
}
(3) 针对 null 和 undefined 的类型保护

在条件判断中,ts 会自动对 null 和 undefined 进行类型保护:

function fn2 (x?: string) {
  if (x) {
    return x.length
  }
}
2.2.3 类型断言
(1) 针对 null 和 undefined 的类型断言

如果我们已经知道的参数不为空,可以使用!来手动标记:

function fn2 (x?: string) {
  return x!.length
}

2.3 索引类型(Index types)

  • keyof
  • in
  • []
(1) keyof

keyof 也是一个 类型关键词 ,可以用来取得一个对象接口的所有 key 值:

interface Person {
  name: string
  age: number
}

type PersonAttrs = keyof Person // 'name' | 'age'
(2) in

in 也是一个 类型关键词 ,可以对 联合类型 进行遍历,只可以用在 type 关键词下面。

type Person = {
  [key in 'name' | 'age']: number
}

// ===>

type Person = {
  name: number;
  age: number;
}
(3) []

使用 [] 操作符可以进行索引访问,也是一个 类型关键词

interface Person {
  name: string
  age: number
}

type x = Person['name'] // x is string

示例
写一个类型复制的类型工具:

type Copy<T> = {
  [key in keyof T]: T[key]
}

interface Person {
  name: string
  age: number
}

type Person1 = Copy<Person>

3、类型推断

ts 中的类型推断是非常强大,而且其内部实现也是非常复杂的。

基本类型推断:

// ts 推导出 x 是 number 类型
let x = 10

对象类型推断:

// ts 推断出 myObj 的类型: myObj: { x: number; y: string; z: boolean;
const myObj = {
  x:1,
  y: '2',
  z: true
}

函数类型推断:

// ts 推导出函数返回值是 number 类型
function len (str: string) {
  return str.length
}

上下文类型推断:

// ts 推导出 event 是 ProgressEvent 类型
const xhr = new XMLHttpRequest()
xhr.onload = function (event) {}

写 ts 代码的时候,对于基本类型我们可以不用手动声明其类型,让 ts 自己去推断

知乎 - typescript上下文相关类型浅析 https://zhuanlan.zhihu.com/p/84481228

4、类型兼容性

typescript 的子类型是基于 结构子类型 的,只要结构可以兼容,就是子类型。(Duck Type)

class Point {
  x: number
}

function getPointX(point: Point) {
  return point.x
}

class Point2 {
  x: number
}

let point2 = new Point2()
getPointX(point2) // OK

java 、 c++ 等传统静态类型语言是基于 名义子类型 的,必须显示声明子类型关系(继承),才可以兼容。

public class Main {
  public static void main (String[] args) {
    getPointX(new Point()); // ok
    getPointX(new ChildPoint()); // ok
    getPointX(new Point1()); // error
  }

  public static void getPointX (Point point) {
    System.out.println(point.x);
  }
  
  static class Point {
    public int x = 1;
  }
  
  static class Point2 {
    public int x = 2;
  }
  
  static class ChildPoint extends Point {
    public int x = 3;
  }
}

4.1 对象子类型

子类型中必须包含源类型所有的属性和方法:

function getPointX(point: { x: number }) {
  return point.x
}

const point = {
  x: 1,
  y: '2'
}

getPointX(point) // OK

注意,如果直接传入一个对象字面量是会报错的:

function getPointX(point: { x: number }) {
  return point.x
}

getPointX({ x: 1, y: '2' }) // error

这是 ts 中的另一个特性,叫做: excess property check ,当传入的参数是一个对象字面量时,会进行额外属性检查。

4.2 协变与逆变

介绍类型兼容和类型安全就非常有必要介绍一下逆变与协变的概念:

在介绍之前,先约定如下标记:

  • A ≼ B 表示A是B的子类型,A包含B的所有属性和方法。
  • A => B 表示以 A 为参数,B 为返回值的方法。 (param: A) => B

一个问题
如果我现在有三个类型 Animal 、 Dog 、 WangCai(旺财) ,那么肯定存在下面的关系: WangCai ≼ Dog ≼ Animal 即 旺财属于狗属于动物。

问题:以下哪种类型是 Dog => Dog 的子类呢?

  • WangCai => WangCai
  • WangCai => Animal
  • Animal => Animal
  • Animal => WangCai

从代码来看

class Animal {
  sleep: Function
}

class Dog extends Animal {
  // 吠
  bark: Function
}

class WangCai extends Dog {
  dance: Function
}

type DogCbFn = (dog: Dog) => Dog

function getDogName (cb: DogCbFn) {
  const dog = new Dog()
  const myDog = cb(dog)
  myDog.bark()
}

const animal = new Animal()
const wangcai = new WangCai()

getDogName(wangcai => {
  wangcai.dance()
  return wangcai
}) // error

getDogName(wangcai => {
  wangcai.dance()
  return animal
}) // error

getDogName(animal => {
  animal.sleep()
  return wangcai
}) // ok

getDogName(animal => {
  animal.sleep()
  return animal
}) // error

可以看到只有 Animal => WangCai 才是 Dog => Dog 的子类型,可以得到一个结论,对于函数类型来说,函数参数的类型兼容是反向的,我们称之为 逆变 ,返回值的类型兼容是正向的,称之为 协变 。

(1) 函数子类型

上面逆变与协变的例子介绍了函数参数只有一个时的情况,如果函数参数有多个时该如何区分?

其实函数的参数可以转化为 Tuple 的类型兼容性:

type Tuple1 = [string, number]
type Tuple2 = [string, number, boolean]

let tuple1: Tuple1 = ['1', 1]
let tuple2: Tuple2 = ['1', 1, true]

let t1: Tuple1 = tuple2 // ok
let t2: Tuple2 = tuple1 // error

可以看到 Tuple2 => Tuple1 ,即长度大的是长度小的子类型,再由于函数参数的逆变特性,所以函数参数少的可以赋值给参数多的(参数从前往后需一一对应):

[1, 2].forEach((item, index) => {
  console.log(item)
}) // ok

[1, 2].forEach((item, index, arr, other) => {
  console.log(other)
}) // error

四、接口

写过java的同学应该都知道接口的概念,java中的接口是用来描述类的功能的

interface BabyInter {
  void cry();
}

class Baby implements BabyInter {
  @Override
  public void cry() {
    System.out.print("crying")
  }
}

但是ts里面的接口还是有一点不一样的,同样是用来描述数据的结构,ts的接口不仅可以描述类,还可以描述变量,描述方法, 使用interface关键词来声明接口:

interface BabyInter {
  name: string,
  cry () : void
}

let baby: BabyInter = {
  name: 'apple',
  cry () : void {
    console.log(this.name + 'is crying')
  }
}

let getBaby = () : BabyInter => {
  return {
    name: 'apple',
    cry () {}
  }
}

1、可选属性

接口里的属性不全都是必需的,可以是可选的。

interface FontStyleOptions {
  size: number;
  color?: string;
}

const getFontStyle = (options: FontStyleOptions) => {
  options.color = options.color || '#000000'
  return options
}

const fontStyle = getFontStyle({
  size: 16
})

可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是在引用了不存在的属性时可以捕获错误。

2、可索引的类型

额外的属性检查 如果FontStyleOptions接口中除了有size属性,还有一些不确定的其它属性,那么可以这样定义它:

interface FontStyleOptions {
  size: number;
  [prop: string]: number;
}
let obj: FontStyleOptions = {
  size: 16,
  border: 1
}
obj.height = 18 // ok
obj.background = '#ffffff' // error 不能将类型“"#ffffff"”分配给类型“number”

要注意的是,对于可索引的类型来说,索引签名的参数类型必须为 “string” 或 “number”:

interface FontStyleOptions {
  size: number;
  [prop: boolean]: string; // error
}

这也很容易理解,因为对象的键值要么是字符串要么是数字。 还要注意的是,索引的类型必须是其他属性的联合类型:

interface FontStyleOptions {
  size: number; // error 类型“number”的属性“size”不能赋给字符串索引类型“string”
  [prop: string]: string;
}

interface FontStyleOptions {
  size: number;
  [prop: string]: string | number; // ok
}

五、类

1、继承

class Animal {
    name: string;
    constructor(theName: string) { this.name = theName; }
    move(distanceInMeters: number = 0) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

class Snake extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 5) {
        console.log("Slithering...");
        super.move(distanceInMeters);
    }
}

class Horse extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 45) {
        console.log("Galloping...");
        super.move(distanceInMeters);
    }
}

let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");

sam.move();
tom.move(34);

输出

Slithering...
Sammy the Python moved 5m.
Galloping...
Tommy the Palomino moved 34m.

关键点

  • 使用extends关键字创建了Animal的两个子类: Horse和 Snake
  • 派生类包含了一个构造函数,它必须调用 super(),它会执行基类的构造函数。 而且,在构造函数里访问 this的属性之前,一定要调用 super()
  • 重写从 Animal继承来的 move方法,使得 move方法根据不同的类而具有不同的功能

ts中的类和es6的类没有什么太大的区别,主要是增加了类成员和方法的修饰符(private, protected, public)和对接口的实现。

2、修饰符

  • public 修饰符: 类成员和方法能在该类和该类的子类中被使用,也能在外部被使用。TypeScript里,成员都默认为 public
  • private 修饰符: 类成员和方法只能在该类中被使用,不能在外部被使用。
  • protected 修饰符: 类成员和方法只能在该类和该类的子类中被使用,不能在外部被使用。
  • readonly关键字将属性设置为只读的,只读属性必须在声明时或构造函数里被初始化
class Person {
  private age: number
  protected name: string

  constructor (name: string, age: number) {
    this.name = name
    this.age = age
  }
}
class Student extends Person {
  constructor (name: string, age: number) {
    super(name, age)
  }
  
  learn () :void {
    console.log(this.name + 'is learning') // ok console.log(this.age) // error
  }
}

let student = new Student('Lucy', 18)
student.name // error

注意
如果其中一个类型里包含一个 private成员,那么只有当另外一个类型中也存在这样一个 private成员, 并且它们都是来自同一处声明时,我们才认为这两个类型是兼容的。 对于 protected成员也使用这个规则

3、存取器

通过getters/setters来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问

let passcode = "secret passcode";
class Employee {
    private _fullName: string;

    get fullName(): string {
        return this._fullName;
    }

    set fullName(newName: string) {
        if (passcode && passcode == "secret passcode") {
            this._fullName = newName;
        }
        else {
            console.log("Error: Unauthorized update of employee!");
        }
    }
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
    alert(employee.fullName);
}

注意

  • 存取器要求将编译器设置为输出ECMAScript 5或更高。 不支持降级到ECMAScript 3
  • 只带有 get不带有 set的存取器自动被推断为 readonly(这在从代码生成 .d.ts文件时是有帮助的,因为利用这个属性的用户会看到不允许够改变它的值)

4、静态属性

前面讨论了类的实例成员,那些仅当类被实例化的时候才会被初始化的属性。 我们也可以使用static关键词创建类的静态成员,这些属性存在于类本身上面而不是类的实例上

class Grid {
    static origin = {x: 0, y: 0};
    calculateDistanceFromOrigin(point: {x: number; y: number;}) {
        let xDist = (point.x - Grid.origin.x);
        let yDist = (point.y - Grid.origin.y);
        return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
    }
    constructor (public scale: number) { }
}

let grid1 = new Grid(1.0);  // 1x scale
let grid2 = new Grid(5.0);  // 5x scale

console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));

5、抽象类

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

    constructor(public name: string) {
    }

    printName(): void {
        console.log('Department name: ' + this.name);
    }

    abstract printMeeting(): void; // 必须在派生类中实现
}

class AccountingDepartment extends Department {

    constructor() {
        super('Accounting and Auditing'); // 在派生类的构造函数中必须调用 super()
    }

    printMeeting(): void {
        console.log('The Accounting Department meets each Monday at 10am.');
    }

    generateReports(): void {
        console.log('Generating accounting reports...');
    }
}


let department: AccountingDepartment = new AccountingDepartment(); // 允许对一个抽象子类进行实例化和赋值
department.printName();
department.printMeeting();

let department2: Department; // 允许创建一个对抽象类型的引用
department2 = new Department(); // 错误: 不能创建一个抽象类的实例
department2.generateReports(); // 错误: 方法在声明的抽象类中不存在

6、类和接口

ts中类可以使用关键词implements来实现接口:

interface BabyInter {
  name: string
  cry () : void
}

class Baby implements BabyInter {
  name: string
  
  constructor (name: string) {
    this.name = name
  }

  cry () :void {
    console.log(this.name + 'is crying')
  }
}

如果 Baby 类中没有声明 cry 方法会怎样:

interface BabyInter {
  name: string
  cry () : void
}

class Baby implements BabyInter {
  name: string
  
  constructor (name: string) {
    this.name = name
  }
}

会报一个编译错误: [ts] 类“Baby”错误实现接口“BabyInter”。类型“Baby”中缺少属性“cry”。

六、函数

1、函数类型

(1) 几种函数类型的返回值类型写法

记住在 () 后面添加返回值类型即可

function fn(): number {
  return 1
}

const fn = function (): number {
  return 1
}

const fn = (): number => {
  return 1
}

const obj = {
  fn (): number {
    return 1
  }
}

(2) 函数类型

ts 中也有函数类型,用来描述一个函数:

type FnType = (x: number, y: number) => number

完整的函数写法

let myAdd: (x: number, y: number) => number = function(x: number, y: number): number {
  return x + y
}

// 使用 FnType
let myAdd: FnType = function(x: number, y: number): number {
  return x + y
}

// ts 自动推导参数类型
let myAdd: FnType = function(x, y) {
  return x + y
}

2、函数重载?

js因为是动态类型,本身不需要支持重载,ts为了保证类型安全,支持了函数签名的类型重载。即:

多个 重载签名 和一个 实现签名

function len(s: string): string;
function len(arr: any[]): number;

function len(x: string | any[]) {
  if (typeof x === 'string') {
    return x.length.toString()
  }
  return x.length
}

let a = len('').trim() // error
let b = len([]).toFixed(2) // ok
let c = len(2) // error

ts 函数重载的其他特性:

  • 如果定义了 重载签名 ,则 实现签名 对外不可见
  • 实现签名 必须兼容 重载签名
  • 重载签名 的类型不会合并

TS 中的函数重载其实并不是真的重载。

七、泛型

1、什么是泛型

软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑代码块的重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型。

通过字面意思来看,泛型有“广泛的类型”之意,即数据的类型也可以通过一个变量来动态确定,一般用 <> 操作符来声明泛型的使用。

小试牛刀
// 泛型方法

function createList<T>(): T[] {
  return [] as T[]
}

const numberList = createList<number>() // number[]
const stringList = createList<string>() // string[]

2、泛型接口

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

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

let myIdentity: GenericIdentityFn<number> = identity;

3、泛型类

// 泛型类的定义
class ArrayList<T> {
  list: T[]

  push (item: T) :void {
    this.list.push(item)
  }

  pop () :T {
    return this.list.pop()
  }
}

// 泛型类的使用
const list = new ArrayList<string>()
list.push('hello') // ok
list.push(1) //error

小知识:K,V,T,E等泛型名称不是固定写死了,是可以随意定义的,为了语义化,是有特殊含义的:

  • T表示 type 类型。
  • K、V:分别代表键值中的 Key、Value
  • E:表示 enum 枚举。

4、泛型约束

如果我们只希望 createList 函数只能生成指定的类型数组,该如何做,可以使用 extends 关键词来约束泛型的范围和形状。

此时 extends 约束的是输入值

interface Lengthwise {
  length: number
}

function createList<T extends string | number | Lengthwise>(): T[] {
  return [] as T[]
}

const numberList = createList<number>() // ok
const arrayList = createList<any[1, 2, 3]>() // ok
const stringList = createList<boolean>() // error

console.log(numberList) // []
console.log(arrayList) // []

4.1 条件控制

extends 除了做约束类型,还可以做条件控制,相当于与一个三元运算符,只不过是针对类型的。

T extends U ? X : Y

如果 T 可以被分配给 U,则返回 X,否则返回 Y。

type IsNumber<T> = T extends number ? true : false

type x = IsNumber<string> // false

4.2 类型映射

类型映射相当与一个类型的函数,可以做一些类型运算,输入一个类型,输出另一个类型,前文我们举了个 Copy 的例子。

(1) 内置的映射类型
// 每一个属性都变成可选
type Partial<T> = {
  [P in keyof T]?: T[P]
}

// 每一个属性都变成可选
type Readonly<T> = {
  readonly [P in keyof T]: T[P]
}

// 选则对象中的某些属性
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
}

// ......

typescript 2.8 在 lib.d.ts 中内置了几个映射类型:

  • Exclude<T, U> – 从 T 中剔除可以赋值给 U 的类型。
  • Extract<T, U> – 提取 T 中可以赋值给 U 的类型。
  • NonNullable<T> – 从 T 中剔除 null 和 undefined 。
  • ReturnType<T> – 获取函数返回值类型。
  • InstanceType<T> – 获取构造函数类型的实例类型。
(2) extends 条件分发

对于 T extends U ? X : Y 来说,还存在一个特性,当 T 是一个联合类型时,会进行条件分发。

type Union = string | number
type ParamType<T> = T extends number ? 'isNumber' : 'isString'

type UnionType = ParamType<Union> // 'isNumber' | 'isString'

实际上,extends 运算会变成如下形式:

 (string extends number ? 'number' : 'string') | (number extends number ? 'number' : 'string')

Extract 就是基于此特性实现的,再配合 never 幺元的特性:

type Exclude<T, K> = T extends K ? never : T

type T1 = Exclude<string | number | boolean, string | boolean> // number
(3) infer 关键词

infer 关键词常在条件类型中和 extends 关键词一同出现,表示将要推断的类型,作为类型变量可以在三元表达式的 True 部分引用

ts 中内置的一个映射类型 ReturnType ,用于获取函数类型的返回值:

function len (str: string) {
  return str.length
}

type LenFn = typeof len // (str: string) => number

type LenFnReturn = ReturnType<LenFn> // number

ReturnType 的实现:

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

八、模块

1、全局模块 vs. 文件模块

默认情况下,我们所写的代码是位于全局模块下的:

const foo = 2

此时,如果我们创建了另一个文件,并写下如下代码,ts 认为是正常的:

const bar = foo // ok

如果要打破这种限制,只要文件中有 import 或者 export 表达式即可:

export const bar = foo // error

2、模块解析策略

Tpescript 有两种模块的解析策略: NodeClassic

tsconfig.jsonmodule 设置成 AMDSystemES2015 时,默认为 classic ,否则为 Node ,也可以使用 moduleResolution 手动指定模块解析策略。

两种模块解析策略的区别在于:

import { a } from 'moduleB'

Classic:

  • /root/src/folder/moduleB.ts
  • /root/src/folder/moduleB.d.ts
  • /root/src/moduleB.ts
  • /root/src/moduleB.d.ts
  • /root/moduleB.ts
  • /root/moduleB.d.ts
  • /moduleB.ts
  • /moduleB.d.ts

Node:

  • /root/src/node_modules/moduleB.ts
  • /root/src/node_modules/moduleB.tsx
  • /root/src/node_modules/moduleB.d.ts
  • /root/src/node_modules/moduleB/package.json (如果指定了"types"属性)
  • /root/src/node_modules/moduleB/index.ts
  • /root/src/node_modules/moduleB/index.tsx
  • /root/src/node_modules/moduleB/index.d.ts
  • /root/node_modules/moduleB.ts
  • /root/node_modules/moduleB.tsx
  • /root/node_modules/moduleB.d.ts
  • /root/node_modules/moduleB/package.json (如果指定了"types"属性)
  • /root/node_modules/moduleB/index.ts
  • /root/node_modules/moduleB/index.tsx
  • /root/node_modules/moduleB/index.d.ts
  • /node_modules/moduleB.ts
  • /node_modules/moduleB.tsx
  • /node_modules/moduleB.d.ts
  • /node_modules/moduleB/package.json (如果指定了"types"属性)
  • /node_modules/moduleB/index.ts
  • /node_modules/moduleB/index.tsx
  • /node_modules/moduleB/index.d.ts

参考:


最后, 希望大家早日实现:成为编程高手的伟大梦想!
欢迎交流~

微信公众号

本文版权归原作者曜灵所有!未经允许,严禁转载!对非法转载者, 原作者保留采用法律手段追究的权利!
若需转载,请联系微信公众号:连先生有猫病,可获取作者联系方式!