feng xiaohan
对象的类型(接口)

在 TypeScript 中,我们使用接口(Interface)来定义对象的类型。

用于匹配两个对象是否相等。

interface Person {
  name: string;
  age: number;
}

let tom: Person = {
  name: "Tom",
  age: 25,
};

上面的例子中,我们定义了一个接口 Person,接着定义了一个变量 tom,它的类型是 Person。这样,我们就约束了 tom 的形状必须和接口 Person 一致。

接口一般首字母大写。

赋值的时候,变量的形状必须和接口的形状保持一致

  • 对象的属性要完全和 interface 里定义的相同
  • 如果有同名的 interface,则会自动产生合并

可选属性

有时我们希望不要完全匹配一个形状,那么可以用可选属性(**?**):

interface Person {
  name: string;
  age?: number;
}

let tom: Person = {
  name: "Tom",
};
interface Person {
  name: string;
  age?: number;
}

let tom: Person = {
  name: "Tom",
  age: 25,
};

可选属性的含义是该属性可以不存在但仍然不允许添加未定义的属性

索引签名——定义任意属性(*)

有时候我们希望一个接口允许有任意的属性(不确定还有什么类型的其他值),可以使用如下方式:

interface Person {
  name: string;
  age?: number;
  [propName: string]: any;
}

let tom: Person = {
  name: "Tom",
  gender: "male",
  a: 1,
  b: "456",
};

使用 [propName: string] 定义了任意属性取 string 类型的值。

注意:一旦定义了任意属性(索引签名),那么确定属性和可选属性的类型都必须是它的类型的子集,例:

interface Person {
  name: string;
  age?: number;
  [propName: string]: string; // 其中的类型必须是string或是其子类型,不然就会报错,所以一般写any
}

let tom: Person = {
  name: "Tom",
  age: 25,
  gender: "male",
};

// index.ts(3,5): error TS2411: Property 'age' of type 'number' is not assignable to string index type 'string'.
// index.ts(7,5): error TS2322: Type '{ [x: string]: string | number; name: string; age: number; gender: string; }' is not assignable to type 'Person'.
//   Index signatures are incompatible.
//     Type 'string | number' is not assignable to type 'string'.
//       Type 'number' is not assignable to type 'string'.

一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:

interface Person {
  name: string;
  age?: number;
  [propName: string]: string | number;
}

let tom: Person = {
  name: "Tom",
  age: 25,
  gender: "male",
};

只读属性

对于可有可无的值,可以使用?来定义

有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly定义只读属性

被赋予只读的属性,只能被访问,不能被修改。

interface Person {
  readonly id: number; // 只读属性
  name: string;
  age?: number;
  [propName: string]: any;
}

let tom: Person = {
  id: 89757,
  name: "Tom",
  gender: "male",
};

tom.id = 9527;

// index.ts(14,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.

上例中,使用 readonly 定义的属性 id 初始化后,又被赋值了,所以报错了。

注意,只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候

interface Person {
  readonly id: number;
  name: string;
  age?: number;
  [propName: string]: any;
}

let tom: Person = {
  name: "Tom",
  gender: "male",
};

tom.id = 89757;

// index.ts(8,5): error TS2322: Type '{ name: string; gender: string; }' is not assignable to type 'Person'.
//   Property 'id' is missing in type '{ name: string; gender: string; }'.
// index.ts(13,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.

上例中,报错信息有两处,第一处是在对 tom 进行赋值的时候,没有给 id 赋值。

第二处是在给 tom.id 赋值的时候,由于它是只读属性,所以报错了。

接口继承——extends

被继承的接口会合并到当前接口里。

interface A extends B {
  name: string;
  age: number;
  readonly id: number;
}

interface B {
  sex: string;
}

let a: A = {
  id: 2,
  name: "张三",
  age: 18,
  sex: "男",
};

函数定义

使用 interface 定义函数类。

interface Fn {
  (name: string): number[];
}
  • ():定义函数传递的参数名称和类型;
  • number[]:定义了函数返回值的类型,此处为 number 类型的数组;
const fn: Fn = function (name: string) {
  return [1];
};
声明文件

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

背景

  • 声明文件是形如xx.d.ts的文件类型;

  • 有些第三方库需要并没有编写好声明文件,需要自己去获取;如果是流行的库,可能网上有别人(社区)写好的声明文件,可以尝试下载,声明文件一般为@type/库名;如果是不活跃的库,可能需要我们手动去声明;

    例如:为 express 手写一个声明库(见示例)

书写声明文件

在不同的场景下,声明文件的内容和使用方式会有所区别。

库的使用场景主要有以下几种:

  • 全局变量:通过 <script> 标签引入第三方库,注入全局变量
  • npm 包:通过 import foo from 'foo' 导入,符合 ES6 模块规范
  • UMD 库:既可以通过 <script> 标签引入,又可以通过 import 导入
  • 直接扩展全局变量:通过 <script> 标签引入后,改变一个全局变量的结构
  • 在 npm 包或 UMD 库中扩展全局变量:引用 npm 包或 UMD 库后,改变一个全局变量的结构
  • 模块插件:通过 <script>import 导入后,改变另一个模块的结构

全局变量

注意:所有声明语句中只能定义类型,切勿在声明语句中定义具体的实现

  • declare var

    用来定义一个全局变量的类型

    // src/jQuery.d.ts
    
    declare let jQuery: (selector: string) => any;
    // src/index.ts
    
    jQuery("#foo");
    // 使用 declare let 定义的 jQuery 类型,允许修改这个全局变量
    jQuery = function (selector) {
      return document.querySelector(selector);
    };
    
  • declare function

    用来定义全局函数的类型。Query 其实就是一个函数,所以也可以用 function 来定义:

    // src/jQuery.d.ts
    declare function jQuery(selector: string): any;
    
    // src/index.ts
    jQuery("#foo");
    
  • declare class

    当全局变量是一个类的时候,我们用 declare class 来定义它的类型:

    // src/Animal.d.ts
    
    declare class Animal {
      name: string;
      constructor(name: string);
      sayHi(): string;
    }
    // src/index.ts
    
    let cat = new Animal("Tom");
    
  • declare enum

    使用 declare enum 定义的枚举类型也称作外部枚举(Ambient Enums)。

    // src/Directions.d.ts
    
    declare enum Directions {
      Up,
      Down,
      Left,
      Right,
    }
    // src/index.ts
    
    let directions = [
      Directions.Up,
      Directions.Down,
      Directions.Left,
      Directions.Right,
    ];
    
  • declare namespace(淘汰)

  • 嵌套的命名空间(*)

  • interface 和 type

    除了全局变量之外,可能有一些类型我们也希望能暴露出来。在类型声明文件中,我们可以直接使用 interfacetype 来声明一个全局的接口或类型12

    // src/jQuery.d.ts
    
    interface AjaxSettings {
      method?: "GET" | "POST";
      data?: any;
    }
    declare namespace jQuery {
      function ajax(url: string, settings?: AjaxSettings): void;
    }
    

    这样的话,在其他文件中也可以使用这个接口或类型了:

    // src/index.ts
    
    let settings: AjaxSettings = {
      method: "POST",
      data: {
        name: "foo",
      },
    };
    jQuery.ajax("/api/post_something", settings);
    

    typeinterface 类似。

  • 防止命名冲突

    暴露在最外层的 interfacetype 会作为全局类型作用于整个项目中,我们应该尽可能的减少全局变量或全局类型的数量。故最好将他们放到 namespace

  • 声明合并

UMD 库

既可以通过 <script> 标签引入,又可以通过 import 导入的库,称为 UMD 库。相比于 npm 包的类型声明文件,我们需要额外声明一个全局变量,为了实现这种方式,ts 提供了一个新语法 export as namespace

示例

  • 为 express 手写一个声明库

    // index.ts
    import express from "express";
    
    const app = express();
    const router = express.Router();
    
    app.use("/api", router);
    router.get("/api", (req: any, res: any) => {
      res.json({
        code: 200,
      });
    });
    app.listen(9002, () => {
      console.log("9002");
    });
    
    // express.d.ts
    declare module "express" {
      interface Router {
        get(path: string, cb: (req: any, res: any) => void): void;
      }
      interface App {
        use(path: string, router: any): void;
        listen(port: number, cb?: () => void);
      }
      interface Express {
        (): App;
        Router(): Router;
      }
      const express: Express;
    
      export default express;
    }
    
命名空间(namespace)

使用命名空间避免全局变量造成的污染。

TS 和 ES6 一样,任何包含顶级的import或者export的文件都被当成一个模块;相反,如果一个文件不带有顶级的import或者export声明,它的内容被视为全局可见,文件和文件之间也是可见的。

// index.ts
const a = 1;
// index2.ts
const a = 2; // 报错

可以使用export避免这种情况:

// index2.ts
export const a = 2;

命名空间特性

  • 使用namespace定义命名空间;
  • 命名空间内部的类默认是私有的,需通过export暴露出来才能使用;
  • 命名空间相当于内部模块,其主要作用是组织代码,避免命名冲突;
// index2.ts
namespace B {
  export const a = 2;
}

console.log(B.a); // 命名空间变量使用
// index.ts
namespace A {
  export const a = 1;
}

console.log(A.a); // 命名空间变量使用

嵌套命名空间

namespace A {
  export namespace C {
    export const D = 5;
  }
}
console.log(A.C.D);

命名空间导出和引入

  • 导出

    // index2.ts
    export namespace B {
      export const a = 2;
    }
    
  • 引入

    import { B } from "./index2";
    console.log(B); // { a: 2 }
    

命名空间别名——简化命名空间

namespace A {
  export namespace C {
    export const D = 5;
  }
}
import AC = A.C; // 为A.C起别名
console.log(AC.D); // 5
// console.log(A.C.D) // 5

注意:ts-node 不能识别命名空间别名。

命名空间重名

如果两个命名空间具有相同的名称,则它们会自动合并

namespace A {
  export const a1 = 1;
}
namespace A {
  export const a2 = 2;
}

等价于:

namespace A {
  export const a1 = 1;
  export const a2 = 2;
}

合并规则包括将同名接口、类和函数合并为一个接口、类或函数,并将同名变量合并为一个变量。

内置对象

JavaScript 中有很多内置对象,它们可以直接在 TypeScript 中当做定义好了的类型。

ECMAScript 的内置对象

ECMAScript 标准提供的内置对象有:

BooleanErrorDateRegExp 等。

我们可以在 TypeScript 中将变量定义为这些类型:

let b: Boolean = new Boolean(1);
let e: Error = new Error("Error occurred");
let d: Date = new Date();
let r: RegExp = /[a-z]/;
let xhr: XMLHttpRequest = new XMLHttpRequest();
let local: Storage = localStorage;
let lo: Location = location;
let promise: Promise<string> = new Promise((r) => r("张三"));
promise.then((res) => {
  // res类型为string
});
let cookie: string = document.cookie;

DOM 和 BOM 的内置对象

DOM 和 BOM 提供的内置对象有:

DocumentHTMLElementEventNodeList 等。

TypeScript 中会经常用到这些类型:

let body: HTMLElement = document.body;
let allDiv: NodeList = document.querySelectorAll("div");
document.addEventListener("click", function (e: MouseEvent) {
  // Do something
});

例如:

document.addEventListener("click", function (e) {
  console.log(e.targetCurrent);
});

// index.ts(2,17): error TS2339: Property 'targetCurrent' does not exist on type 'MouseEvent'.

上面的例子中,addEventListener 方法是在 TypeScript 核心库中定义的:

interface Document
  extends Node,
    GlobalEventHandlers,
    NodeSelector,
    DocumentEvent {
  addEventListener(
    type: string,
    listener: (ev: MouseEvent) => any,
    useCapture?: boolean
  ): void;
}
  • 定义某个常见 DOM 元素

    // HTML(元素名称)Element、HTMLElement、Element
    let div: HTMLInputElement = document.querySelector("input");
    
  • 定义获取的一组 DOM 元素

    let div: NodeList = document.querySelector("input");
    
  • 定义获取一组类型不固定的 DOM 元素

    let div: NodeListOf<HTMLDivElement | HTMLElement> =
      document.querySelector("div footer");
    

用 TypeScript 写 Node.js

Node.js 不是内置对象的一部分,如果想用 TypeScript 写 Node.js,则需要引入第三方声明文件:

npm install @types/node --save-dev
函数的类型

函数声明(定义)

一个函数有输入和输出,要在 TypeScript 中对其进行约束,需要把输入和输出都考虑到,其中函数声明的类型定义较简单:

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

注意,输入多余的(或者少于要求的)参数,是不被允许的

function sum(x: number, y: number): number {
  return x + y;
}
sum(1, 2, 3);

// index.ts(4,1): error TS2346: Supplied parameters do not match any signature of call target.

函数表达式

let mySum = function (x: number, y: number): number {
  return x + y;
};
// 实际
let mySum: (x: number, y: number) => number = function (
  x: number,
  y: number
): number {
  return x + y;
};

在 TypeScript 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。

箭头函数

let add = (a: number, b: number): number => a + b;

用接口定义函数的形状

我们也可以使用接口的方式来定义一个函数需要符合的形状:

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

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

采用函数表达式|接口定义函数的方式时,对等号左侧进行类型限制,可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变。

可选参数

与接口中的可选属性类似,我们用 ? 表示可选的参数:

function buildName(firstName: string, lastName?: string) {
  if (lastName) {
    return firstName + " " + lastName;
  } else {
    return firstName;
  }
}
let tomcat = buildName("Tom", "Cat");
let tom = buildName("Tom");

注意:可选参数后面不允许再出现必须参数。

参数默认值

在 ES6 中,我们允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数

function buildName(firstName: string, lastName: string = "Cat") {
  return firstName + " " + lastName;
}
let tomcat = buildName("Tom", "Cat");
let tom = buildName("Tom");

此时就不受「可选参数必须接在必需参数后面」的限制了:

function buildName(firstName: string = "Tom", lastName: string) {
  return firstName + " " + lastName;
}
let tomcat = buildName("Tom", "Cat");
let cat = buildName(undefined, "Cat");

注意:可选参数和默认参数不能同时使用。

剩余参数(*)

ES6 中,可以使用 ...rest 的方式获取函数中的剩余参数(rest 参数):

function push(array, ...items) {
  items.forEach(function (item) {
    array.push(item);
  });
}

let a: any[] = [];
push(a, 1, 2, 3);

事实上,items 是一个数组。所以我们可以用数组的类型来定义它:

function push(array: any[], ...items: any[]) {
  items.forEach(function (item) {
    array.push(item);
  });
}

let a = [];
push(a, 1, 2, 3);

this

ts 允许定义 this 的类型(js 不能),函数的第一个参数可以定义 this 的类型。

interface Obj {
  user: number[];
  add: (this: Obj, num: number) => void; // 定义this的类型为Obj,传递的参数类型为number,函数没有返回值
}
let obj: Obj = {
  user: [1, 2, 3],
  add(this: Obj, num: number) {
    this.user.push(num);
  },
};

obj.add(4);

函数重载

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

let users: number[] = [1, 23, 4];
function findNum(): number[]; // 如果什么都不传入则查询全部
function findNum(id: number): number[]; // 传入id则根据id查询
function findNum(add: number[]): number[]; // 传入number类型数组就将其添加到数组里
function findNum(id?: number | number[]): number[] {
  if (typeof id == "number") {
    return users.filter((uId) => uId == id);
  } else if (Array.isArray(id)) {
    user.push(...id);
  } else {
    return user;
  }
}

console.log(findNum()); // [1,23,4]
console.log(findNum(4)); // [4]
console.log(findNum([6, 7, 9])); // [1,23,4,6,7,9]
三斜线指令

三斜线指令是包含单个 XML 标签的单行注释。注释的内容会做为编译器指令使用。

  • 三斜线指令仅可放在包含它的文件的最顶端。
  • 一个三斜线指令的前面只能出现单行或多行注释,这包括其它的三斜线指令。如果它们出现在一个语句或声明之后,那么它们会被当做普通的单行注释,并且不具有特殊的涵义。
  • ///<reference path="...">指令:用于声明文件间的依赖

    // index1.ts
    namespace A {
      export const a = 1;
    }
    
    // index2.ts
    namespace A {
      export const b = 1;
    }
    
    ///<reference path="index1.ts">
    ///<reference path="index2.ts">
    
    console.log(A);
    
  • /// <reference types="module-name" />:用于引入第三方模块的类型声明文件。

    npm install @types/node -D
    
    /// <reference types="node" />
    
Webpack + TS

安装

  • webpack:

    npm install webpack -D
    
  • webpack-cli:webpack4 以上需要

    npm install  webpack-cli -D
    
  • TS 编译器:帮助解析 ts 文件

    npm install ts-loader -D
    
  • TS 环境:

    npm install typescript -D
    
  • 热更新服务

    npm install  webpack-dev-server -D
    
  • HTML 模板:

    npm install html-webpack-plugin -D
    

配置

需要在 webpack.config.js 里配置相关信息:

const path = require("path");
const htmlWebpackPlugin = require("html-webpack-plugin"); // 热更新插件
module.exports = {
  entry: "./src/index.ts", // 入口文件
  mode: "development", // 开发模式
  output: {
    // 出口(输出)文件信息
    path: path.resolve(__dirname, "./dist"), // 出口文件夹
    filename: "index.js", // 出口文件名
  },
  stats: "none",
  resolve: {
    extensions: [".ts", ".js"], // 匹配后缀 @1
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
  module: {
    rules: [
      // 匹配编译器,如ts、scss等
      {
        test: /\.ts$/, // 正则匹配文件
        use: "ts-loader", // 使用ts-loader对文件进行处理
      },
    ],
  },
  devServer: {
    port: 1988, // 启动的端口号
    proxy: {}, // 代理
  },
  plugins: [
    new htmlWebpackPlugin({
      // 热更新
      template: "./public/index.html", // 指定热更新模板(文件)
    }),
  ],
};

@1:在import时自动查找文件,不用再加上后缀。比如 index.js 可以直接写 index。

启动命令

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "webpack-dev-server", // 启动webpack-dev-server,持续热更新项目
    "build":"webpack" // 打包
},
TypeScript项目构建

构建 TS 项目

  1. 下载 ts

    npm install -g typescript
    
  2. 构建 ts 文件,创建.ts扩展名的文件并编写 ts 代码

    ​ 例如文件叫greeter.ts

  3. 编译代码:将ts文件输出为js文件

    tsc greeter.ts
    

    正常情况下会出现一个.js 的同名文件

  4. 搭载 node 运行 js 文件

    node greeter.js
    

解决每次使用都进行tsc转换

  • 将项目初始化一个 npm 进行管理

    npm init -y
    
  • packge.json中 编写命令

    {
      "name": "lesson01",
      "version": "1.0.0",
      "description": "",
      "main": "greeter.js",
      "scripts": {
        "build": "tsc greeter.ts",
        "test": "node greeter.js",
        "start": "npm run build && npm run test"
      },
      "keywords": [],
      "author": "",
      "license": "ISC"
    }
    

TS 命令行语句

查看 TS 版本

tsc -v

实时编译 TS

由于浏览器是不认识 ts 文件的,所以可以使用以下指令来监听 ts 文件,将其实时编译成 js 文件。

tsc -w

TS 配置文件

生成一个 ts 的配置文件:tsconfig.json

tsc --init

在这个文件中可以设置:

  • strict:严格模式

TS 库

  • ts-node

    可以直接运行 ts 文件,不需要将其手动编译为 js。

    npm i ts-node -g
    

    使用:

    ts-node 文件名
    
  • @types/node

    使用 ts 写 node.js。

    Node.js 不是内置对象的一部分,如果想用 TypeScript 写 Node.js,则需要引入第三方声明文件。

    npm i @types/node -D
    
TS高级-类型兼容(协变、逆变、双向协变)

类型兼容

所谓的类型兼容性,就是用于确定一个类型是否能赋值给其他的类型。typeScript 中的类型兼容性是基于结构类型的(也就是形状),如果 A 要兼容 B 那么 A 至少具有 B 相同的属性。

协变

如果类型 S 可以赋值给类型 T,那么我们就说类型 T 是类型 S 的协变类型。简单来说,就是子类型可以赋值给父类型,子类型中的属性必须完全覆盖父类型中的属性

而这类型也被称为鸭子类型(一只鸟走路像鸭子,游泳也像,做什么都像鸭子,那么这只鸟就可以成为鸭子类型)。

// 父类型
interface A {
  name: string;
  age: number;
}
// 子类型
interface B {
  name: string;
  age: number;
  sex: string;
}
let a: A = {
  name: "张三",
  age: 18,
};
let b: B = {
  name: "李四",
  age: 19,
  sex: "女",
};
// 父 = 子
a = b; // 协变

协变是针对值的。

逆变

协变赋值的相反操作,对于函数来说,拥有父类型的函数赋值给子类型的函数:

let fnA = (params: A) => {};
let fnB = (params: B) => {};
// 子 = 父
fnB = fnA; // 逆变

逆变是针对函数(函数参数)的。

双向协变

是子类型和父类型可以互相赋值。

注意:这在 TypeScript2.0 之前允许这么操作,TypeScript2.0 之后发现这样是不安全的,如果你需要支持双向协变的话需要自己去开启这个属性:

// tsconfig.json
"compilerOptions": {
    "strictFunctionTypes": false,
}
TS高级-Record和Readonly

Record

定义一个键值对类型的对象(即可以同时对键和值进行类型约束)。

type Person = {
  name: string;
  age: number;
};
type K = "A" | "B" | "C";

type P = Record<K, Person>;

let person: P = {
  A: { name: "张三", age: 18 },
  B: { name: "李四", age: 20 },
  C: { name: "王五", age: 22 },
};

原理

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

keyof any会返回:

type key = string | number | symbol;

Readonly

用于定义一个只读类型的对象(即对象的属性值不能被修改)。

type MyObj = {
  readonly a: string;
  readonly b: number;
};

const obj: MyObj = {
  a: "hello",
  b: 123,
};

obj.a = "world"; // Error: Cannot assign to 'a' because it is a read-only property.