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 里面的属性。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
1. TypeScript 是一种由微软开发的开源编程语言,它是 JavaScript 的一个超集。它添加了静态类型和面向对象编程的特性,并提供了更强大的工具和功能来帮助开发者编写可维护和可扩展的代码。 2. TypeScript 的优势和用途: - 静态类型检查:TypeScript 引入了静态类型系统,可以在编译时捕获一些常见的错误。这可以提供更好的代码质量和可靠性,并减少在运行时出现的错误。 - 更好的代码维护性:TypeScript 提供了类、接口、模块等面向对象编程的特性,使得代码更易于组织和维护。它还支持代码重构、智能感知和代码导航等功能,提高了开发效率。 - 渐式采用:TypeScript 可以与 JavaScript 无缝集成,可以逐步将现有的 JavaScript 代码迁移到 TypeScript,而无需重写全部代码。这使得团队可以逐步采用 TypeScript,而不需要一次性投入大量时间和资源。 - 社区支持和生态系统:TypeScript 拥有庞大的开发者社区和丰富的第三方库支持,可以轻松地与其他流行的 JavaScript 框架和工具行集成。 3. TypeScript 与 JavaScript 的关系: TypeScript 是 JavaScript 的超集,这意味着任何有效的 JavaScript 代码都是有效的 TypeScript 代码。TypeScript 扩展了 JavaScript,添加了静态类型、类、接口等新的语法特性,并提供了更丰富的工具和功能来提高开发效率和代码质量。TypeScript 代码可以通过编译器将其转换为 JavaScript 代码,这样可以在任何支持 JavaScript 的环境中运行。因此,开发者可以使用 TypeScript 来编写更可靠、可维护和可扩展的代码,同时仍然可以享受到 JavaScript 生态系统的优势。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值