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 不能够正确的识别。
此时不靠安装,自己手动写一个类型定义文件,帮助 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 里面的属性。