02.TypeScript 语法进阶

TypeScript 语法进阶

1. TypeScript 中的配置文件

1.1 初始化 TS 项目和配置项

初始化
tsc --init

执行后会在当前目录生成 TS 配置文件 tsconfig.json

指定文件夹/文件编译

只有直接使用 tsc 编译,不指明编译的文件,才会根据配置文件的配置项来进行编译。

如果 tsc 指定要编译的文件,如 tsc xxx.ts,就不会去看配置项直接编译。

注意下,ts-node 是会看配置项进行编译的。

如果要指定编译的文件,又要用配置文件的配置项,在配置文件中配置要编译/不想编译的 TS 文件,然后 tsc 即可。

{
  "include": [
    // src 下的任何目录下的任何文件
    "src/**/*"
  ],
  "exclude": [
    "node_modules"
  ],
  "files": [...],
  "compilerOptions": {
    ......
}
compilerOptions

常用的配置:

"removeComments": true // 编译后去除注释
"noImplicitAny": true // 不允许 ts 有 any 类型,避免成为 AnyScript 有效方式
"strictNullChecks": true // null 校验,变量不能被赋值为 null
"rootDir": "./",  // 指定代码的根文件夹
"outDir": "./build",  // 指定编译的文件放在哪个文件夹
"incremental": true, // 只编译没编译后的内容
"allowJs": true,  // 支持编译 JS,适用于 TS 项目里有 JS 文件的情况
"checkJs": true,  // 支持 JS 语法检查

2. 联合类型和类型保护

联合类型

如果输入的类型可以是两种类型,那就用到了联合类型:

interface Bird {
  fly: boolean;
  sing: () => {};
}

interface Dog {
  fly: boolean;
  bark: () => {};
}

function trainAnimal(animal: Bird | Dog) {
  // animal.sing 会报错,因为 animal 并不一定是 Bird 的实例
}

联合类型会对后边 TS 的静态分析造成一定的问题,因此需要人工操作进行类型保护。

例如,上面的代码比较难受了,只会提示 fly,其他的两个方法则会报错,因为万一是 Bird 调用的 bark 方法就肯定执行不了了。

通过断言进行类型保护

用断言,确信此时的变量确信为 Dog 或者 Bird。这就是类型保护,确保代码不再报错。

function trainAnimal(animal: Bird | Dog) {
  // 会飞是鸟,否则是狗
  if (animal.fly) {
    (animal as Bird).sing();
  } else {
    (animal as Dog).bark();
  }
}
in 语法进行类型保护

判断该实例内是否有某种特定方法

function trainAnimal(animal: Bird | Dog) {
  // 有 sing 是 Bird,否则是 Dog
  if ("sing" in animal) {
    (animal as Bird).sing();
  } else {
    (animal as Dog).bark();
  }
}
typeof 语法进行类型保护

以下的情况,如果参数可能是字符串也有可能是数字的情况下,TS 会因为不知道如何进行加法如何处理而报错。

function add(first: string | number, second: string | number) {
  return first + add; // 会报错
}

用 typeof 来判断基本数据类型,可以避免这个问题:

function add(first: string | number, second: string | number) {
  // 如果至少有一个变量是字符串类型,则进行拼接即可
  if (typeof first === "string" || typeof second === "string") {
    return `${first}${second}`;
  }
  return first + second;
}
instanceof 语法进行类型保护

JS 基础,就不多说了。typeof 适用于基础类型判断,而 instance 适用于引用类型判断。

class numberObj {
  public count: number;

  constructor(count: number) {
    this.count = count;
  }
}

function add2(first: object | numberObj, second: object | numberObj) {
  if (first instanceof numberObj && second instanceof numberObj) {
    return first.count + second.count;
  }
  return 0;
}

3. Enum 枚举类型

现在有一个需求,根据状态码标识返回一个结果,方法可以这样写:

function getResult(status: number) {
  if (status === 0) {
    return "offline";
  } else if (status === 1) {
    return "online";
  } else if (status === 2) {
    return "deleted";
  }
  return "error";
}

但是这方法未免太憨了,可读性很低,而且用数字容易搞混。这时就需要用到枚举类型。

enum Status {
  OFFLINE,
  ONLINE,
  DELETED,
}

function getResult(status: number) {
  if (status === Status.OFFLINE) {
    return "offline";
  } else if (status === Status.ONLINE) {
    return "online";
  } else if (status === Status.DELETED) {
    return "deleted";
  }
  return "error";
}

const result = getResult(0);
console.log(result);

这里的返回结果为 offline,因为默认值为 index。index 为 0 的时候是 OFFLINE。当然,枚举成员的值可以自己设置。

enum Status {
  OFFLINE = 4,
  ONLINE = 5,
  DELETED = 6,
}

function getResult(status: number) {
  if (status === Status.OFFLINE) {
    return "offline";
  } else if (status === Status.ONLINE) {
    return "online";
  } else if (status === Status.DELETED) {
    return "deleted";
  }
  return "error";
}

const result = getResult(5);
console.log(result); // 返回 online

还有一个用法,反查枚举里的成员:

enum Status {
  OFFLINE = 4,
  ONLINE = 5,
  DELETED = 6,
}

console.log(Status[6]); // DELETED

4. 函数泛型

泛型指的是泛指的类型,等调用函数的时候指定类型才知道参数要什么类型的。说土一点,类型是一个变量,调用的时候需要传入类型。

function join<T>(first: T, second: T): T {
  return `${first}${second}`;
}

join<string>("1", "1");

如上代码,上面的函数的类型是不确定的,但是可以确定两个参数的类型必须要一样。这时候用泛型就很合适了,两个参数都是 T 泛型,但是 T 是什么,等函数调用的时候才知道。

泛型可以继续加工:

// 参数类型填 T[] 也
function map<T>(params: Array<T>) {
  return params;
}

map<string>(["123"]);

这里传入的参数必须是泛型数组。

函数泛型可以指定多个:

function join<T, P>(first: T, second: P) {
  return `${first}${second}`;
}

// TS 能推断出传入的参数是否正确
join(1, "1");

5. 类中的泛型

同样的,不仅函数有泛型,类也有泛型。毕竟类中也需要存储数据结构。

示例代码如下:

class DataManager<T> {
  constructor(private data: T[]) {}

  getItem(index: number): T {
    return this.data[index];
  }
}

const data= new DataManager<string>(["hello", "world"]);

不确定存入的类型是啥样的,但是确信 data 里的元素只能是同种类型,类的泛型就用得上了。

泛型继承

现在有个需求,要获取 item 里的 name 属性,那么表明了,传入的参数必须是携带有 name 属性的对象,那么搭配泛型应该怎么解决呢?

interface Item {
  name: string;
}

class DataManager<T extends Item> {
  constructor(private data: T[]) {}

  getItemName(index: number): string {
    return this.data[index].name;
  }
}

const data= new DataManager([{name: "sjh"}]);

声明一个 Item 接口,指明必须要有 name 属性,然后 T 继承 Item 后,T 也必须要有 name 属性了。

6. 命名空间 namespace

过多的全局变量会使得代码难以维护,如以下代码:

class Header {
  constructor() {
    const elem = document.createElement("div");
    elem.innerText = "this is a header";
    document.body.appendChild(elem);
  }
}

class Content {
  constructor() {
    const elem = document.createElement("div");
    elem.innerText = "this is a content";
    document.body.appendChild(elem);
  }
}

class Footer {
  constructor() {
    const elem = document.createElement("div");
    elem.innerText = "this is a footer";
    document.body.appendChild(elem);
  }
}

class Page {
  constructor() {
    new Header();
    new Content();
    new Footer();
  }
}

new Page();

全局真正用到的类就只有 Page,但是 Header、Content、Footer 都变成了全局变量。

这里就需要有模块化的思想,每个模块相互独立,不会生成全局变量。

// 声明一个命名空间
namespace Home {
  class Header {
    constructor() {
      const elem = document.createElement("div");
      elem.innerText = "this is a header";
      document.body.appendChild(elem);
    }
  }

  class Content {
    constructor() {
      const elem = document.createElement("div");
      elem.innerText = "this is a content";
      document.body.appendChild(elem);
    }
  }

  class Footer {
    constructor() {
      const elem = document.createElement("div");
      elem.innerText = "this is a footer";
      document.body.appendChild(elem);
    }
  }

  // 只想暴露 Page 到外界
  export class Page {
    constructor() {
      new Header();
      new Content();
      new Footer();
    }
  }
}

new Home.Page();

7. 使用 Parcel 打包 TS 代码

Parcel 我们可以当做是轻量版的 webpack,将写的兼容性较差的代码转化为浏览器可以理解的 JS 文件。

安装 parcel:

yarn global add parcel-bundler

在 scripts 里用 Parcel 运行 html 文件:

  "scripts": {
    "test": "parcel ./src/index.html"
  }

表示用 Parcel 对 HTML 的内容进行编译,使得浏览器能够理解。此时,HTML 可以直接引入 TS 文件而不会报错。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>Document</title>
  <script src="./page.ts"></script>
</head>

<body>
</body>

</html>

运行的时候会提供一个服务器地址来显示网页:

8. 描述文件中的全局类型

写 .d.ts 类型定义文件,用来适配 JS 库。

例如,现在在 TS 文件里写 jQuery 代码:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>Document</title>
  <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
  <script src="./page.ts"></script>
</head>

<body>
</body>

</html>
// page.ts
$(function () {
  alert(123);
});

用 Parcel 来运行,是能够成功运行代码的。

但是问题是,因为是通过 JS 文件引入,TS 不能够正确的识别。

image-20220524165030100

此时不靠安装,自己手动写一个类型定义文件,帮助 TS 理解 jQuery。

创建 jquery.d.ts 类型定义文件。

定义全局变量
declare var $: (params: () => void) => void;

表示有一个全局变量 $,是一个函数,接收一个返回为空的函数,返回为空。

现在下面的的代码就不会报错了,因为可以识别。

$(function () {
  alert(123);
});
定义全局函数
declare function $(params: () => void): void;

定义一个全局函数,意思和上面的一样。

函数的重载

jQuery 用 $ 函数可以获取节点,并且链式调用:

$(function () {
  $("body").html("<div>123</div>")
});

此时,$ 函数需要接收一个 string 类型。因此得多写一个声明来适配。

declare function $(params: () => void): void;

declare function $(params: string): {
  html: (string) => {};
};

第二个声明表示,$ 函数接收一个 string 类型,返回一个对象,里边有 html 方法,html 方法里边需要接收一个 string 类型。

代码优化

链式调用方法一股脑放在函数声明里可读性较差,链式调用的灵魂也没表示出来,另外,参数名也得写的形象化一点。因此可以这样写:

interface JqueryInstance {
  html: (html: string) => JqueryInstance;
}

// 函数重载
declare function $(readyFunc: () => void): void;
declare function $(selector: string): JqueryInstance;
interface 实现函数重载的定义
interface JqueryInstance {
  html: (html: string) => JqueryInstance;
}

interface JQuery {
  (readyFunc: () => void): void;
  (selector: string): JqueryInstance;
}

declare var $: JQuery;
命名空间定义对象,以及和类的类型定义方法

jquery 比较原生的写法如下:

$(function () {
  new $.fn.init();
});

如果要让 TS 识别,可以这样写:

declare function $(readyFunc: () => void): void;

declare namespace $ {
  namespace fn {
    class init {}
  }
}

表示有个 $,里面有 fn,fn 里有 init 的构造函数可以 new 出一个对象。

9. 模块代码的类型描述文件

现在用 yarn add jquery 的方式来安装 jQuery,此时就用 ES6 的 import 语法直接引入 jQuery 即可。

import $ from "jquery";

$(function () {
  new $.fn.init();
});

现在类型描述文件的写法就得变化了。

// ES6 模块化
declare module "jquery" {
  interface JqueryInstance {
    html: (html: string) => JqueryInstance;
  }

  function $(readyFunc: () => void): void;
  function $(selector: string): JqueryInstance;

  namespace $ {
    namespace fn {
      class init {}
    }
  }

  export = $;
}

里边代表了 jquery 模块的类型描述,并暴露了 $。

10. 泛型中 keyof 语法的使用

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

class Teacher {
  constructor(private info: Person) {}
  getInfo(key: string) {
    // 这一行 TS 报错了
    return this.info[key];
  }
}

const teacher = new Teacher({
  name: "John",
  age: 18,
  gender: "male",
});

const info = teacher.getInfo("name");
console.log(info);

这个代码是能正常运行的,但是 TS 会报错,原因在于,getInfo 传进来的参数 key 并不能保证是 Person 接口内指定的属性。TS 认为这样是不安全的。

泛型结合 keyof 解决问题
getInfo<T extends keyof Person>(key: T): Person[T]

用 keyof 对 Person 里的属性进行遍历。

等价于:

getInfo<T extends "name">(key: T): Person[T] // T = "name"
getInfo<T extends "age">(key: T): Person[T] // T = "age"
getInfo<T extends "gender">(key: T): Person[T] // T = "gender"

此时的 T 是一个确切的字符串,避免了风险。现在可以得知,类型可以精确为固定的字符串。

总而言之,功能是,keyof 要求参数是指定的 interface 里面的属性。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值