前端知识图谱 - Typescript总结
一、搭建 TypeScript 学习环境
1.1 安装 TypeScript
npm i -g typescript
tsc -v // Version 4.0.2
1.2 安装ts-node
npm i -g ts-node
1.3 创建 tsconfig.json 文件
tsc --init
1.4 编译 TypeScript 文件
tsc helloworld.ts
helloworld.ts => helloworld.js
1.5 TypeScript 初体验
新建一个 hello.ts 文件,并输入以下内容:
function greet(person: string) {
return 'Hello, ' + person;
}
console.log(greet("TypeScript"));
然后执行 tsc hello.ts 命令,之后会生成一个编译好的文件 hello.js:
"use strict";
function greet(person) {
return 'Hello, ' + person;
}
console.log(greet("TypeScript"));
二、TypeScript 基础类型
2.1 JS八种内置类型
let num: number = 0;
let str: string = 'string';
let bool: boolean = false;
let n: null = null;
let u: undefined = undefined;
let obj: object = {a: 1};
let big: bigint = 100n; // es2020
let sym: symbol = Symbol('sym');
- 默认情况下 null 和 undefined 是所有类型的子类型,可以赋值给其他任何类型;如果 tsconfig.json 指定了"strictNullChecks":true,null 和 undefined 只能赋值给 void 和它们各自的类型;
- number 和 bigint 都表示数字,但是这两个类型不兼容;
2.2 Array 类型
let list: number[] = [1, 2];
// 泛型数组
let list: Array<string> = ['1', '2'];
// 联合类型数组
let list: (number | string)[] = [1, '2'];
// 指定对象成员的数组
interface Item {
name: string,
age: number
}
let list: Item[] = [{ name: 'ts', age: 18 }]
2.3 Enum 类型
1.数字枚举
enum Direction {
NORTH,
SOUTH,
EAST,
WEST,
}
let dirName = Direction[0]; // NORTH
let dirVal: Direction = Direction["NORTH"]; // 0
let dir: Direction = Direction.NORTH; // 0
默认情况下,NORTH 的初始值为 0,其余的成员会自增;也可以设置 NORTH 的初始值,比如:
enum Direction {
NORTH = 3,
SOUTH,
EAST,
WEST,
}
2.字符串枚举 在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。
enum Direction {
NORTH = "NORTH",
SOUTH = "SOUTH",
EAST = "EAST",
WEST = "WEST",
}
以上代码对应的 ES5 代码如下:
"use strict";
var Direction;
(function (Direction) {
Direction["NORTH"] = "NORTH";
Direction["SOUTH"] = "SOUTH";
Direction["EAST"] = "EAST";
Direction["WEST"] = "WEST";
})(Direction || (Direction = {}));
3.常量枚举
使用const关键字修饰的枚举,常量枚举会使用内联语法,不会为枚举类型编译生成任何 JavaScript;
const enum Direction {
NORTH = "NORTH",
SOUTH = "SOUTH",
EAST = "EAST",
WEST = "WEST",
}
let dir: Direction = Direction.NORTH;
以上代码对应的 ES5 代码如下:
"use strict";
let dir = "NORTH" /* NORTH */;
4.异构枚举 异构枚举的成员值是数字和字符串的混合:
enum Enum {
A,
B,
C = "C",
D = "D",
E = 5,
F,
}
以上代码对于的 ES5 代码如下:
"use strict";
var Enum;
(function (Enum) {
Enum[Enum["A"] = 0] = "A";
Enum[Enum["B"] = 1] = "B";
Enum["C"] = "C";
Enum["D"] = "D";
Enum[Enum["E"] = 5] = "E";
Enum[Enum["F"] = 6] = "F";
})(Enum || (Enum = {}));
通过观察上述生成的 ES5 代码,我们可以发现数字枚举相对字符串枚举多了 “反向映射”:
console.log(Enum.A) //输出:0
console.log(Enum[0]) // 输出:A
注意: 类型为不同枚举的变量不可以对比
enum e1 { A, B }
enum e2 { A, B }
let a: e1.A = 0
let b: e2.A = 0
a === b // This condition will always return 'false' since the types 'e1' and 'e2' have no overlap.(2367)
2.4 Tuple 类型
元组是类型约束更精确的数组,用于定义具有有限数量的未命名属性的类型,每个属性都有一个关联的类型,使用元组时,必须提供每个属性的值;
1.元组定义
let TupleType: [string, boolean, number?];
TupleType = ["ts", true];
console.log(TupleType[0]); // ts
console.log(TupleType[1]); // true
在元组初始化的时候,如果出现类型不匹配会提示错误信息,比如:
TupleType = [true, "ts"];
// [0]: Type 'true' is not assignable to type 'string'.
// [1]: Type 'string' is not assignable to type 'boolean'.
在元组初始化的时候,我们还必须提供每个属性的值,不然也会出现错误,比如:
TupleType = ["ts"];
// Property '1' is missing in type '[string]' but required in type '[string, boolean]'.
2.元组剩余元素
type RestTupleType = [number, ...string[]];
let restTuple: RestTupleType = [666, "ts", "Kakuqo", "Lolo"];
console.log(restTuple[0]);
console.log(restTuple[1]);
3.只读元组
const point: readonly [number, number] = [10, 20];
// Cannot assign to '0' because it is a read-only property.
point[0] = 1;
// Property 'push' does not exist on type 'readonly [number, number]'.
point.push(0);
// Property 'pop' does not exist on type 'readonly [number, number]'.
point.pop();
// Property 'splice' does not exist on type 'readonly [number, number]'.
point.splice(1, 1);
4.数组转换为元组
使用as const将数组类型转换为元组:
const a = 1
const b = 'b'
const arr = [a, b] as const //元组类型
5.元组越界问题
let arr: [string, number] = ['ts', 1];
arr.push(2); // ok
console.log(arr); // ok ['ts', 1, 2];
console.log(arr[2]); // error 打印添加的元素时会报错
2.5 Any 类型
在 TypeScript 中,任何类型都可以被归为 any 类型。这让 any 类型成为了类型系统的顶级类型(也被称作全局超级类型)。
let a: any = 1;
a = "a";
a = false;
any 类型本质上是类型系统的一个逃逸舱。作为开发者,这给了我们很大的自由:TypeScript 允许我们对 any 类型的值执行任何操作,而无需事先执行任何形式的检查。比如:
let value: any;
value.foo.bar; // OK
value.trim(); // OK
value(); // OK
new value(); // OK
value[0][1]; // OK
使用 any 类型,可以很容易地编写类型正确但在运行时有问题的代码。如果我们使用 any 类型,就无法使用 TypeScript 提供的大量的保护机制。为了解决 any 带来的问题,TypeScript 3.0 引入了 unknown 类型。
2.6 Unknown 类型
就像所有类型都可以赋值给 any,所有类型也都可以赋值给 unknown。这使得 unknown 成为 TypeScript 类型系统的另一种顶级类型(另一种是 any)
let value: unknown;
value = 1; // OK
value = "string"; // OK
value = true; // OK
value = null; // OK
value = undefined; // OK
value = []; // OK
value = {}; // OK
value = Math.random; // OK
value = new TypeError(); // OK
value = Symbol("type"); // OK
对 value 变量的所有赋值都被认为是类型正确的。但是,当我们尝试将类型为 unknown 的值赋值给其他类型的变量时会发生什么?
let value: unknown;
let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error
let value5: string = value; // Error
let value6: object = value; // Error
let value7: any[] = value; // Error
let value8: Function = value; // Error
unknown 类型只能被赋值给 any 类型和 unknown 类型本身。直观地说,这是有道理的:只有能够保存任意类型值的容器才能保存 unknown 类型的值。毕竟我们不知道变量 value 中存储了什么类型的值。 现在让我们看看当我们尝试对类型为 unknown 的值执行操作时会发生什么。以下是我们在之前 any 章节看过的相同操作:
let value: unknown;
value.foo.bar; // Error
value.trim(); // Error
value(); // Error
new value(); // Error
value[0][1]; // Error
将 value 变量类型设置为 unknown 后,这些操作都不再被认为是类型正确的。通过将 any 类型改变为 unknown 类型,我们已将允许所有更改的默认设置,更改为禁止任何更改。
unknown 与 any 的最大区别是:
任何类型的值可以赋值给any,同时any类型的值也可以赋值给任何类型。
unknown 任何类型的值都可以赋值给它,但它只能赋值给 unknown 和 any
2.7 Void 类型
某种程度上来说,void 类型与 any 类型相反,表示没有任何类型。当一个函数没有返回值时,其返回值类型是 void:
// 声明函数返回值为void
function fn(): void {
console.log("no return");
}
需要注意的是,声明一个 void 类型的变量没有什么作用,因为在严格模式下,它的值只能为 undefined:
let unusable: void = undefined;
2.8 Never 类型
never 类型表示的是那些永不存在的值的类型(相当于空的联合类型),如抛出异常、死循环
// 异常
function err(msg: string): never { // OK
throw new Error(msg);
}
// 死循环
function loopForever(): never { // OK
while (true) {};
}
never 类型同 null 和 undefined 一样,也是任何类型的子类型,也可以赋值给任何类型。但是没有类型是never的子类型或可以赋值给never类型(除了never本身之外),即使any也不可以赋值给never
let ne: never;
let nev: never;
let an: any;
ne = 123; // Error
ne = nev; // OK
ne = an; // Error
在 TypeScript 中,可以利用 never 类型的特性来实现全面性检查,具体示例如下:
type Foo = string | number;
function controlFlowAnalysisWithNever(foo: Foo) {
if (typeof foo === "string") {
// 这里 foo 被收窄为 string 类型
} else if (typeof foo === "number") {
// 这里 foo 被收窄为 number 类型
} else {
// foo 在这里是 never
const check: never = foo;
}
}
注意在 else 分支里面,我们把收窄为 never 的 foo 赋值给一个显示声明的 never 变量。如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来修改了 Foo 的类型:
type Foo = string | number | boolean;
然而没有同时修改 controlFlowAnalysisWithNever 方法中的控制流程,这时候 else 分支的 foo 类型会被收窄为 boolean 类型,导致无法赋值给 never 类型,这时就会产生一个编译错误。通过这个方式,我们可以确保 controlFlowAnalysisWithNever 方法总是穷尽了 Foo 的所有可能类型。 通过这个示例,我们可以得出一个结论:使用 never 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。
2.9 Object, object 和 {} 类型
1.Object 类型 Object 类型:它是所有 Object 类的实例的类型,它由以下两个接口来定义:
Object 接口定义了 Object.prototype 原型对象上的属性;
// node_modules/typescript/lib/lib.es5.d.ts
interface Object {
constructor: Function;
toString(): string;
toLocaleString(): string;
valueOf(): Object;
hasOwnProperty(v: PropertyKey): boolean;
isPrototypeOf(v: Object): boolean;
propertyIsEnumerable(v: PropertyKey): boolean;
}
ObjectConstructor 接口定义了 Object 类的属性。
// node_modules/typescript/lib/lib.es5.d.ts
interface ObjectConstructor {
/** Invocation via `new` */
new(value?: any): Object;
/** Invocation via function calls */
(value?: any): any;
readonly prototype: Object;
getPrototypeOf(o: any): any;
// ···
}
declare var Object: ObjectConstructor;
Object 类的所有实例都继承了 Object 接口中的所有属性。
2.object 类型 object 类型是:TypeScript 2.2 引入的新类型,它用于表示非原始类型。
// node_modules/typescript/lib/lib.es5.d.ts
interface ObjectConstructor {
create(o: object | null): any;
// ...
}
const proto = {};
Object.create(proto); // OK
Object.create(null); // OK
Object.create(undefined); // Error
Object.create(1337); // Error
Object.create(true); // Error
Object.create("oops"); // Error
3.{} 类型 {} 类型描述了一个没有成员的对象。当你试图访问这样一个对象的任意属性时,TypeScript 会产生一个编译时错误。
// Type {}
const obj = {};
// Error: Property 'prop' does not exist on type '{}'.
obj.prop = "ts";
但是,你仍然可以使用在 Object 类型上定义的所有属性和方法,这些属性和方法可通过 JavaScript 的原型链隐式地使用:
// Type {}
const obj = {};
// "[object Object]"
obj.toString();
三、TypeScript 断言
3.1 类型断言
有时候你会遇到这样的情况,你会比 TypeScript 更了解某个值的详细信息。通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。
通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。类型断言好比其他语言里的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。
TypeScript 类型检测无法做到绝对智能,毕竟程序不能像人一样思考。有时会碰到我们比 TypeScript 更清楚实际类型的情况,比如下面的例子:
const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find(num => num > 2); // 提示 ts(2322)
其中,greaterThan2 一定是一个数字(确切地讲是 3),因为 arrayNumber 中明显有大于 2 的成员,但静态类型对运行时的逻辑无能为力。
在 TypeScript 看来,greaterThan2 的类型既可能是数字,也可能是 undefined,所以上面的示例中提示了一个 ts(2322) 错误,此时我们不能把类型 undefined 分配给类型 number。
不过,我们可以使用一种笃定的方式——类型断言(类似仅作用在类型层面的强制类型转换)告诉 TypeScript 按照我们的方式做类型检查。
比如,我们可以使用 as 语法做类型断言,如下代码所示:
const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find(num => num > 2) as number;
类型断言有两种形式:
// 尖括号 语法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
// as 语法(推荐使用)
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
as断言的应用场景
- as const(TypeScript 3.4)
const str = 'str'
/*
str为值类型(str: str),相当于 let str: str = 'str' 或 let str = 'str' as const
*/
let a = 'string'
let b = 18
const arr = [a, b]
/*
arr: (string | number)[]
当数组使用as const之后,const arr = [a, b, true] as const,就会变成居于a、b类型的只读元组
arr: readonly [string, number, true]
*/
const obj = {
name: 'ts',
age: b
} as const
/*
对象使用as const,会转换为 readonly 的值类型
obj: {
readonly name: 'ts',
readonly age: number
}
*/
function func() {
let a = 'string'
let b = (x: number, y: number): number => x + y
return [a, b] as const
}
const [s, f] = func()
f(1, 2)
/*
函数返回值为数组时,使用 as const 可以将返回值里的类型暴露出去,同数组使用 as const
*/
- DOM断言处理
let body = document.querySelector('body') as HTMLBodyElement
let div = document.querySelector('.div') as HTMLDivElement
3.2 非空断言
在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 ! 可以用于断言操作对象是非 null 和非 undefined 类型。
1.忽略 undefined 和 null 类型
function myFunc(maybeString: string | undefined | null) {
// Type 'string | null | undefined' is not assignable to type 'string'.
// Type 'undefined' is not assignable to type 'string'.
const onlyString: string = maybeString; // Error
const ignoreUndefinedAndNull: string = maybeString!; // Ok
}
2.调用函数时忽略 undefined 类型
type NumGenerator = () => number;
function myFunc(numGenerator: NumGenerator | undefined) {
// Object is possibly 'undefined'.(2532)
// Cannot invoke an object which is possibly 'undefined'.(2722)
const num1 = numGenerator(); // Error
const num2 = numGenerator!(); //OK
}
因为 ! 非空断言操作符会从编译生成的 JavaScript 代码中移除,所以在实际使用的过程中,要特别注意。比如下面这个例子:
const a: number | undefined = undefined;
const b: number = a!;
console.log(b);
以上 TS 代码会编译生成以下 ES5 代码:
"use strict";
const a = undefined;
const b = a;
console.log(b);
虽然在 TS 代码中,我们使用了非空断言,使得 const b: number = a!; 语句可以通过 TypeScript 类型检查器的检查。但在生成的 ES5 代码中,! 非空断言操作符被移除了,所以在浏览器中执行以上代码,在控制台会输出 undefined。
3.3 确定赋值断言
在 TypeScript 2.7 版本中引入了确定赋值断言,即允许在实例属性和变量声明后面放置一个 ! 号,从而告诉 TypeScript 该属性会被明确地赋值。为了更好地理解它的作用,我们来看个具体的例子:
let x: number;
initialize();
// Variable 'x' is used before being assigned.(2454)
console.log(2 * x); // Error
function initialize() {
x = 10;
}
很明显该异常信息是说变量 x 在赋值前被使用了,要解决该问题,我们可以使用确定赋值断言:
let x!: number;
initialize();
console.log(2 * x); // Ok
function initialize() {
x = 10;
}
通过 let x!: number; 确定赋值断言,TypeScript 编译器就会知道该属性会被明确地赋值。
四、类型守卫
类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。 换句话说,类型保护可以保证一个字符串是一个字符串,尽管它的值也可以是一个数值。类型保护与特性检测并不是完全不同,其主要思想是尝试检测属性、方法或原型,以确定如何处理值。目前主要有四种的方式来实现类型保护:
4.1 in 关键字
interface Admin {
name: string;
privileges: string[];
}
interface Employee {
name: string;
startDate: Date;
}
type UnknownEmployee = Employee | Admin;
function printEmployeeInformation(emp: UnknownEmployee) {
console.log("Name: " + emp.name);
if ("privileges" in emp) {
console.log("Privileges: " + emp.privileges);
}
if ("startDate" in emp) {
console.log("Start Date: " + emp.startDate);
}
}
4.2 typeof 关键字
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
typeof 类型保护只支持两种形式:typeof v === "typename" 和 typeof v !== typename,"typename" 必须是 "number", "string", "boolean" 或 "symbol"。 但是 TypeScript 并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。
4.3 instanceof 关键字
interface Padder {
getPaddingString(): string;
}
class SpaceRepeatingPadder implements Padder {
constructor(private numSpaces: number) {}
getPaddingString() {
return Array(this.numSpaces + 1).join(" ");
}
}
class StringPadder implements Padder {
constructor(private value: string) {}
getPaddingString() {
return this.value;
}
}
let padder: Padder = new SpaceRepeatingPadder(6);
if (padder instanceof SpaceRepeatingPadder) {
// padder的类型收窄为 'SpaceRepeatingPadder'
}
4.4 自定义类型保护的类型谓词
function isNumber(x: any): x is number {
return typeof x === "number";
}
function isString(x: any): x is string {
return typeof x === "string";
}
五、联合类型和类型别名
5.1 联合类型
使用 | 运算符创建联合类型,通常与 null 或 undefined 一起使用:
const sayHello = (name: string | undefined) => {
/* ... */
};
例如,这里 name 的类型是 string | undefined 意味着可以将 string 或 undefined 的值传递给sayHello 函数。
sayHello("ts");
sayHello(undefined);
通过这个示例,你可以凭直觉知道类型 A 和类型 B 联合后的类型是同时接受 A 和 B 值的类型。此外,对于联合类型来说,你可能会遇到以下的用法:
let num: 1 | 2 = 1;
type EventNames = 'click' | 'scroll' | 'mousemove';
以上示例中的 1、2 或 'click' 被称为字面量类型,用来约束取值只能是某几个值中的一个。
5.2 可辨识联合
TypeScript 可辨识联合(Discriminated Unions)类型,也称为代数数据类型或标签联合类型。它包含 3 个要点:可辨识、联合类型和类型守卫。 这种类型的本质是结合联合类型和字面量类型的一种类型保护方法。如果一个类型是多个类型的联合类型,且多个类型含有一个公共属性,那么就可以利用这个公共属性,来创建不同的类型保护区块。
1.可辨识 可辨识要求联合类型中的每个元素都含有一个单例类型属性,比如:
enum CarTransmission {
Automatic = 200,
Manual = 300
}
interface Motorcycle {
vType: "motorcycle"; // discriminant
make: number; // year
}
interface Car {
vType: "car"; // discriminant
transmission: CarTransmission
}
interface Truck {
vType: "truck"; // discriminant
capacity: number; // in tons
}
在上述代码中,我们分别定义了 Motorcycle、 Car 和 Truck 三个接口,在这些接口中都包含一个 vType 属性,该属性被称为可辨识的属性,而其它的属性只跟特性的接口相关。
2.联合类型 基于前面定义了三个接口,我们可以创建一个 Vehicle 联合类型:
type Vehicle = Motorcycle | Car | Truck;
现在我们就可以开始使用 Vehicle 联合类型,对于 Vehicle 类型的变量,它可以表示不同类型的车辆。
3.类型守卫 下面我们来定义一个 evaluatePrice 方法,该方法用于根据车辆的类型、容量和评估因子来计算价格,具体实现如下:
const EVALUATION_FACTOR = Math.PI;
function evaluatePrice(vehicle: Vehicle) {
return vehicle.capacity * EVALUATION_FACTOR;
}
const myTruck: Truck = { vType: "truck", capacity: 9.5 };
evaluatePrice(myTruck);
对于以上代码,TypeScript 编译器将会提示以下错误信息:
Property 'capacity' does not exist on type 'Vehicle'.
Property 'capacity' does not exist on type 'Motorcycle'.
原因是在 Motorcycle 接口中,并不存在 capacity 属性,而对于 Car 接口来说,它也不存在 capacity 属性。那么,现在我们应该如何解决以上问题呢?这时,我们可以使用类型守卫。下面我们来重构一下前面定义的 evaluatePrice 方法,重构后的代码如下:
function evaluatePrice(vehicle: Vehicle) {
switch(vehicle.vType) {
case "car":
return vehicle.transmission * EVALUATION_FACTOR;
case "truck":
return vehicle.capacity * EVALUATION_FACTOR;
case "motorcycle":
return vehicle.make * EVALUATION_FACTOR;
}
}
在以上代码中,我们使用 switch 和 case 运算符来实现类型守卫,从而确保在 evaluatePrice 方法中,我们可以安全地访问 vehicle 对象中的所包含的属性,来正确的计算该车辆类型所对应的价格。
5.3 类型别名
类型别名用来给一个类型起个新名字。
type Message = string | string[];
let greet = (message: Message) => {
// ...
};
六、交叉类型
在 TypeScript 中交叉类型是将多个类型合并为一个类型。通过 & 运算符可以将现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。交叉类型实现type的继承。
type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };
let point: Point = {
x: 1,
y: 1
}
6.1 同名基础类型属性的合并
那么现在问题来了,假设在合并多个类型的过程中,刚好出现某些类型存在相同基础类型的成员,但对应的类型又不一致,比如:
interface X {
c: string;
d: string;
}
interface Y {
c: number;
e: string
}
type XY = X & Y;
type YX = Y & X;
let p: XY;
let q: YX;
在上面的代码中,接口 X 和接口 Y 都含有一个相同的成员 c,但它们的类型不一致。对于这种情况,此时 XY 类型或 YX 类型中成员 c 的类型是不是可以是 string 或 number 类型呢?比如下面的例子:
p = { c: 6, d: "d", e: "e" };
q = { c: "c", d: "d", e: "e" };
为什么接口 X 和接口 Y 混入后,成员 c 的类型会变成 never 呢?这是因为混入后成员 c 的类型为 string & number,即成员 c 的类型既可以是 string 类型又可以是 number 类型。很明显这种类型是不存在的,所以混入后成员 c 的类型为 never。
6.2 同名非基础类型属性的合并
在上面示例中,刚好接口 X 和接口 Y 中内部成员 c 的类型都是基本数据类型,那么如果是非基本数据类型的话,又会是什么情形。我们来看个具体的例子:
interface D { d: boolean; }
interface E { e: string; }
interface F { f: number; }
interface A { x: D; }
interface B { x: E; }
interface C { x: F; }
type ABC = A & B & C;
let abc: ABC = {
x: {
d: true,
e: 'ts',
f: 666
}
};
console.log('abc:', abc);
/*
{
x: {
d: true,
e: 'ts',
f: 666
}
}
*/
交叉类型真正的用武之地就是将多个接口类型合并成一个类型,从而实现等同接口继承的效果,也就是所谓的合并接口类型,可以将合并接口类型理解为求并集
type IntersectionType = { id: number; name: string; } & { age: number };
const mixed: IntersectionType = {
id: 1,
name: 'name',
age: 18
}
七、TypeScript 函数
7.1 TypeScript 函数与 JavaScript 函数的区别
| TypeScript | JavaScript |
|---|---|
| 含有类型 | 无类型 |
| 箭头函数 | 箭头函数(ES2015) |
| 函数类型 | 无函数类型 |
| 必填和可选参数 | 所有参数都是可选的 |
| 默认参数 | 默认参数 |
| 剩余参数 | 剩余参数 |
| 函数重载 | 无函数重载 |
7.2 箭头函数
参考ES6箭头函数
list.forEach(() => console.log('ts'));
list.forEach(item => console.log(item));
list.forEach((item, index, arr) =>
console.log(index + '-' + item);
);
7.3 参数类型和返回类型
function createUserId(name: string, id: number): string {
return name + id;
}
7.4 可选参数及默认参数
// 可选参数
function createUserId(name: string, id: number, age?: number): string {
return name + id;
}
// 默认参数
function createUserId(
name: string = "ts",
id: number,
age?: number
): string {
return name + id;
}
可选参数要放在普通参数的后面,不然会导致编译错误。
7.5 剩余参数
function push(array, ...items) {
items.forEach(function (item) {
array.push(item);
});
}
let a = [];
push(a, 1, 2, 3);
剩余参数要放在普通参数的后面,不然会导致编译错误。
7.6 用接口定义函数类型
interface SearchFunc{
(source: string, subString: string): boolean;
}
采用函数表达式接口定义函数的方式时,对等号左侧进行类型限制,可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变。
7.7 函数类型
type Fntype = (x: number, y: number) => number
let sum: Fntype = (x: number, y: number): number => x + y;
7.8 函数重载
函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: Combinable, b: Combinable) {
// type Combinable = string | number;
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
在以上代码中,我们为 add 函数提供了多个函数类型定义,从而实现函数的重载。在 TypeScript 中除了可以重载普通函数之外,我们还可以重载类中的成员方法。 方法重载是指在同一个类中方法同名,参数不同(参数类型不同、参数个数不同或参数个数相同时参数的先后顺序不同),调用时根据实参的形式,选择与它匹配的方法执行操作的一种技术。所以类中成员方法满足重载的条件是:在同一个类中,方法名相同且参数列表不同。下面我们来举一个成员方法重载的例子:
class Calculator {
add(a: number, b: number): number;
add(a: string, b: string): string;
add(a: string, b: number): string;
add(a: number, b: string): string;
add(a: Combinable, b: Combinable) {
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
}
const calculator = new Calculator();
const result = calculator.add('ts', ' Kakuqo');
这里需要注意的是,当 TypeScript 编译器处理函数重载时,它会查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。另外在 Calculator 类中,add(a: Combinable, b: Combinable){ } 并不是重载列表的一部分,因此对于 add 成员方法来说,我们只定义了四个重载方法。
八、TypeScript 数组
8.1 数组解构
let x: number; let y: number; let z: number;
let array = [0, 1, 2, 3, 4];
[x, y, z] = array;
8.2 数组展开运算符
let array = [0, 1];
let arr = [...array, 2, 3, 4];
8.3 数组遍历
let array: number[] = [0, 1, 2, 3, 4];
for (let i of array) {
console.log(i);
}
九、TypeScript 对象
9.1 对象解构
let person = {
name: "ts",
gender: "Male",
};
let { name, gender } = person;
9.2 对象展开运算符
let person = {
name: "ts",
gender: "Male",
address: "Xiamen",
};
// 组装对象
let personWithAge = { ...person, age: 33 };
// 获取除了某些项外的其它项
let { name, ...rest } = person;
十、TypeScript 接口
在面向对象语言中,接口是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类去实现。 TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。
对象类型接口、函数类型接口
10.1 对象的形状
interface Person {
name: string;
age: number;
}
let person: Person = {
name: "ts",
age: 18,
};
10.2 可选 | 只读属性
interface Person {
readonly name: string;
age?: number;
}
只读属性用于限制只能在对象刚刚创建的时候修改其值。此外 TypeScript 还提供了 ReadonlyArray<T> 类型,它与 Array<T>相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改。
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!
10.3 任意属性
有时候我们希望一个接口中除了包含必选和可选属性之外,还允许有其他的任意属性,这时我们可以使用 索引签名 的形式来满足上述要求。
interface Person {
name: string;
age?: number;
[propName: string]: any;
}
const p1 = { name: "ts" };
const p2 = { name: "js", age: 5 };
const p3 = { name: "es", sex: 1 }
/*
propName的类型为 number ,一般都代表是数组;
propName的类型为 string ,一般都代表是对象;
*/
10.4 接口与类型别名的区别
1.Objects/Functions
接口和类型别名都可以用来描述对象的形状或函数签名:
// 接口
interface Point {
x: number;
y: number;
}
interface SetPoint {
(x: number, y: number): void;
}
// 类型别名
type Point = {
x: number;
y: number;
};
type SetPoint = (x: number, y: number) => void;
2.Other Types
与接口类型不一样,类型别名可以用于一些其他类型,比如原始类型、联合类型和元组:
// primitive
type Name = string;
// object
type PartialPointX = { x: number; };
type PartialPointY = { y: number; };
// union
type PartialPoint = PartialPointX | PartialPointY;
// tuple
type Data = [number, string];
3.扩展
类型别名通过 &(交叉运算符) 来扩展,而接口通过 extends 的方式来扩展。
// Interface extends interface
interface PartialPointX { x: number; }
interface Point extends PartialPointX {
y: number;
}
// Type alias & type alias
type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };
// Interface extends type alias
type PartialPointX = { x: number; };
interface Point extends PartialPointX { y: number; }
// Type alias & interface
interface PartialPointX { x: number; }
type Point = PartialPointX & { y: number; };
4.Implements 类可以以相同的方式实现接口或类型别名,但类不能实现使用类型别名定义的联合类型:
interface Point {
x: number;
y: number;
}
class SomePoint implements Point {
x = 1;
y = 2;
}
type Point2 = {
x: number;
y: number;
};
class SomePoint2 implements Point2 {
x = 1;
y = 2;
}
type PartialPoint = { x: number; } | { y: number; };
// A class can only implement an object type or
// intersection of object types with statically known members.
class SomePartialPoint implements PartialPoint { // Error
x = 1;
y = 2;
}
5.Declaration merging 接口会合并重复声明,而同名类型别名会冲突;
interface Point { x: number; }
interface Point { y: number; }
const point: Point = { x: 1, y: 2 };
type User = {
name: string;
};
// 标识符“User”重复。ts(2300)
type User = {
id: number;
}
总结
- 定义一个对象或者函数时,用 type 或 interface都可以
- 定义基本类型别名时,用 type
- 使用 in 关键字生成映射类型时,用 type
- 要用组合或者交叉类型(
&)时,用 type - 要用类的
extends或implements时,用 interface - 要合并重复声明时,用 interface
十一、TypeScript 类
11.1 类的属性与方法
在面向对象语言中,类是一种面向对象计算机编程语言的构造,是创建对象的蓝图,描述了所创建的对象共同的属性和方法。 在 TypeScript 中,我们可以通过 Class 关键字来定义一个类:
class Greeter {
// 静态属性
static cname: string = "Greeter";
// 成员属性
greeting: string;
// 构造函数 - 执行初始化操作
constructor(message: string) {
this.greeting = message;
}
// 静态方法
static getClassName() {
return "Class name is Greeter";
}
// 成员方法
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");
那么成员属性与静态属性,成员方法与静态方法有什么区别呢?这里无需过多解释,我们直接看一下编译生成的 ES5 代码:
"use strict";
var Greeter = /** @class */ (function () {
// 构造函数 - 执行初始化操作
function Greeter(message) {
this.greeting = message;
}
// 静态方法
Greeter.getClassName = function () {
return "Class name is Greeter";
};
// 成员方法
Greeter.prototype.greet = function () {
return "Hello, " + this.greeting;
};
// 静态属性
Greeter.cname = "Greeter";
return Greeter;
}());
var greeter = new Greeter("world");
11.2 ECMAScript 私有字段
在 TypeScript 3.8 版本就开始支持ECMAScript 私有字段,使用方式如下:
class Person {
#name: string;
constructor(name: string) {
this.#name = name;
}
greet() {
console.log(`Hello, my name is ${this.#name}!`);
}
}
let ts = new Person("ts");
ts.#name;
// ~~~~~
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.
与常规属性(甚至使用 private 修饰符声明的属性)不同,私有字段要牢记以下规则:
私有字段以 # 字符开头,有时我们称之为私有名称; 每个私有字段名称都唯一地限定于其包含的类; 不能在私有字段上使用 TypeScript 可访问性修饰符(如 public 或 private); 私有字段不能在包含的类之外访问,甚至不能被检测到。
11.3 访问器
在 TypeScript 中,我们可以通过 getter 和 setter 方法来实现数据的封装和有效性校验,防止出现异常数据。
let passcode = "Hello TypeScript";
class Employee {
private _fullName: string;
get fullName(): string {
return this._fullName;
}
set fullName(newName: string) {
if (passcode && passcode == "Hello TypeScript") {
this._fullName = newName;
} else {
console.log("Error: Unauthorized update of employee!");
}
}
}
let employee = new Employee();
employee.fullName = "ts";
if (employee.fullName) {
console.log(employee.fullName);
}
11.4 类的继承
继承(Inheritance)是一种联结类与类的层次模型。指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系。 继承是一种 is-a 关系:
在 TypeScript 中,我们可以通过 extends 关键字来实现继承:
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);
}
}
let sam = new Snake("Sammy the Python");
sam.move();
11.5 抽象类
使用 abstract 关键字声明的类,我们称之为抽象类。抽象类不能被实例化,因为它里面包含一个或多个抽象方法。所谓的抽象方法,是指不包含具体实现的方法:
abstract class Person {
constructor(public name: string){}
abstract say(words: string) :void;
}
// Cannot create an instance of an abstract class.(2511)
const lolo = new Person(); // Error
抽象类不能被直接实例化,我们只能实例化实现了所有抽象方法的子类。具体如下所示:
abstract class Person {
constructor(public name: string){}
// 抽象方法
abstract say(words: string) :void;
}
class Developer extends Person {
constructor(name: string) {
super(name);
}
say(words: string): void {
console.log(`${this.name} says ${words}`);
}
}
const lolo = new Developer("lolo");
lolo.say("I love ts!"); // lolo says I love ts!
11.6 类方法重载
在前面的章节,我们已经介绍了函数重载。对于类的方法来说,它也支持重载。比如,在以下示例中我们重载了 ProductService 类的 getProducts 成员方法:
class ProductService {
getProducts(): void;
getProducts(id: number): void;
getProducts(id?: number) {
if(typeof id === 'number') {
console.log(`获取id为 ${id} 的产品信息`);
} else {
console.log(`获取所有的产品信息`);
}
}
}
const productService = new ProductService();
productService.getProducts(666); // 获取id为 666 的产品信息
productService.getProducts(); // 获取所有的产品信息
十二、TypeScript 泛型
软件工程中,我们不仅要创建一致的定义良好的 API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。
在像 C# 和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。
设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值。
泛型(Generics)是允许同一个函数接受不同类型参数的一种模板。相比于使用 any 类型,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类型。
12.1 泛型语法
对于刚接触 TypeScript 泛型的读者来说,首次看到 <T> 语法会感到陌生。其实它没有什么特别,就像传递参数一样,我们传递了我们想要用于特定函数调用的类型。
其中 T 代表 Type,在定义泛型时通常用作第一个类型变量名称。但实际上 T 可以用任何有效名称代替。除了 T 之外,以下是常见泛型变量代表的意思:
- K(Key):表示对象中的键类型;
- V(Value):表示对象中的值类型;
- E(Element):表示元素类型。
其实并不是只能定义一个类型变量,我们可以引入希望定义的任何数量的类型变量。比如我们引入一个新的类型变量 U,用于扩展我们定义的 identity 函数:
function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}
console.log(identity<Number, string>(68, "ts"));
除了为类型变量显式设定值之外,一种更常见的做法是使编译器自动选择这些类型,从而使代码更简洁。我们可以完全省略尖括号,比如:
function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}
console.log(identity(1, "ts"));
对于上述代码,编译器足够聪明,能够知道我们的参数类型,并将它们赋值给 T 和 U,而不需要开发人员显式指定它们。
12.2 泛型接口
interface GenericIdentityFn<T> {
(arg: T): T;
}
12.3 泛型类
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
return x + y;
};
12.4 关键词的特性
1.typeof
在 TypeScript 中,typeof 操作符可以用来获取一个变量声明或对象的类型。
interface Person {
name: string;
age: number;
}
const sem: Person = { name: 'ts', age: 33 };
type Sem= typeof sem; // -> Person
function toArray(x: number): Array<number> {
return [x];
}
type Func = typeof toArray; // -> (x: number) => number[]
2.keyof索引查询
对应任何类型T, keyof T的结果为该类型上所有公有属性key的联合。
interface Eg1 {
name: string,
readonly age: number,
}
// T1的类型实则是name | age
type T1 = keyof Eg1
class Eg2 {
private name: string;
public readonly age: number;
protected home: string;
}
// T2实则被约束为 age
// 而name和home不是公有属性,所以不能被keyof获取到
type T2 = keyof Eg2
type T3 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join"
type T4 = keyof { [x: string]: Person }; // string | number
在 TypeScript 中支持两种索引签名,数字索引和字符串索引:
interface StringArray {
// 字符串索引 -> keyof StringArray => string | number
[index: string]: string;
}
interface StringArray1 {
// 数字索引 -> keyof StringArray1 => number
[index: number]: string;
}
为了同时支持两种索引类型,就得要求数字索引的返回值必须是字符串索引返回值的子类。其中的原因就是当使用数值索引时,JavaScript 在执行索引操作时,会先把数值索引先转换为字符串索引。所以 keyof { [x: string]: Person } 的结果会返回 string | number。
keyof any得到的是string | number | symbol,原因在于类型key的类型只能是这几个
3.in
in 用来遍历枚举类型:
type Keys = "a" | "b" | "c"
type Obj = {
[p in Keys]: any
} // -> { a: any, b: any, c: any }
4.T[K]索引访问
interface Eg1 {
name: string,
readonly age: number,
}
type V1 = Eg1['name'] // string
type V2 = Eg1['name' | 'age'] // string | number
5.extends
- 泛型约束 通过 extends 关键字添加泛型约束,判断 联合类型 之间的包含关系;
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
// 现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:
loggingIdentity(3); // Error, number doesn't have a .length property
loggingIdentity({length: 10, value: 3}); // OK
- 用于接口,表示继承
interface T1 {
name: string,
}
interface T2 {
sex: number,
}
interface T3 extends T1, T2 { age: number }
// T3 = {name: string, sex: number, age: number}
- 表示条件类型,可用于条件判断(extends + 三元运算符)
type A1 = 'x' extends 'x' ? string : number;
// type A1 = string
type A2 = 'x' | 'y' extends 'x' ? string : number;
// type A2 = number
type P<T> = T extends 'x' ? string : number;
type A3 = P<'x' | 'y'>;
// type A3 = string | number
为什么A2和A3的值不一样? 其实A3相当于:
type A3= ('x' extends 'x' ? string : number) | ('y' extends 'x' ? string : number)
// 通过简单的元组类型包裹避免分解
type P<T> = [T] extends ['x'] ? string : number;
type T4 = P<'x' | 'y'>; // number
参数是泛型且代入参数的是联合类型时,extends就会使用
分配律
6.infer
在条件类型语句中,可以用 infer 声明一个类型变量并且对它进行使用。
type ReturnType<T> = T extends (
...args: any[]
) => infer R ? R : any;
以上代码中 infer R 就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用。
- infer推导的名称相同并且都处于逆变的位置,则推导的结果将会是交叉类型
type Bar<T> = T extends {
a: (x: infer U) => void;
b: (x: infer U) => void;
} ? U : never;
// type T1 = string
type T1 = Bar<{ a: (x: string) => void; b: (x: string) => void }>;
// type T2 = never
type T2 = Bar<{ a: (x: string) => void; b: (x: number) => void }>;
- infer推导的名称相同并且都处于协变的位置,则推导的结果将会是联合类型
type Foo<T> = T extends {
a: infer U;
b: infer U;
} ? U : never;
// type T1 = string
type T1 = Foo<{ a: string; b: string }>;
// type T2 = string | number
type T2 = Foo<{ a: string; b: number }>;
infer关键词只能在extends条件类型上使用,不能在其他地方使用
十三、TypeScript 装饰器
装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。
装饰器工厂是一个简单的函数,它返回一个函数,以供装饰器在运行时调用。
注意:若要启用实验性的装饰器特性,你必须在命令行或tsconfig.json里启用experimentalDecorators编译器选项: 命令行:
tsc --target ES5 --experimentalDecorators
// tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true, // 开启装饰器
"emitDecoratorMetadata": true // 开启元数据
}
}
13.1 类装饰器
declare type ClassDecorator = <TFunction extends Function>(
target: TFunction
) => TFunction | void;
类装饰器顾名思义,就是用来装饰类的。它接收一个参数:
- target: TFunction - 被装饰的类
const PersonDecorator: ClassDecorator = (target: Function): void => {
console.log(target); // [Function: Person]
target.prototype.greet = () => {
console.log('Hello ts!');
}
}
@PersonDecorator
class Person {}
let person = new Person();
(person as any).greet(); // 'Hello ts!';
13.2 属性装饰器
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
属性装饰器顾名思义,用来装饰类的属性。它接收两个参数:
- target: Object - 被装饰的类
- propertyKey: string | symbol - 被装饰类的属性名
const nameProperty: PropertyDecorator = (target: Object, propertyKey: string | symbol): void => {
let value: string
Object.defineProperty(target, propertyKey, {
get: () => {
return `Hello ${value}`
},
set: (v) => {
value = v
}
});
}
class Person {
@nameProperty
public name: string | undefined;
}
const person = new Person();
person.name = 'ts'
console.log(person.name) // Hello ts
13.3 方法装饰器
declare type MethodDecorator = <T>(
target: Object,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;
方法装饰器顾名思义,用来装饰类的方法。它接收三个参数:
- target: Object - 被装饰的类
- propertyKey: string | symbol - 方法名
- descriptor: TypePropertyDescript - 属性描述符
const GreetDecorator = (
target: Object,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
): void => {
let method = descriptor.value;
descriptor.value = () => {
console.log('person greet')
method()
}
}
class Person {
@GreetDecorator
greet() {
console.log('greet')
}
}
const person = new Person();
(person as any).greet(); // person greet; greet
13.4 参数装饰器
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;
参数装饰器顾名思义,是用来装饰函数参数,它接收三个参数:
- target: Object - 被装饰的类
- propertyKey: string | symbol - 方法名
- parameterIndex: number - 方法中参数的索引值
const MsgDeecorator = (target: Function, propertyKey: string, parameterIndex: number): void => {
let name = propertyKey || target.prototype.constructor.name;
console.log(`The parameter in position ${parameterIndex} at ${name} has been decorated`);
// The parameter in position 0 at Person has been decorated
}
class Person {
greeting: string;
constructor(@MsgDeecorator msg: string) {
this.greeting = msg;
}
}
13.5 装饰器工厂
装饰器工厂就是一个简单的函数,它返回一个表达式,以供装饰器在运行时调用。
const DecoratorFactory = (value: string) => {
return (target) => {
// do something with "target" and "value"...
}
}
13.6 装饰器组合
- 由上至下依次对装饰器表达式求值。
- 求值的结果会被当作函数,由下至上依次调用。
const f = () => {
console.log("f(): evaluated");
return (target: Object) => {
console.log("f(): called");
}
}
const g = () => {
console.log("g(): evaluated");
return (target: Object) => {
console.log("g(): called");
}
}
@f()
@g()
class C {}
/*
f(): evaluated
g(): evaluated
g(): called
f(): called
*/
十四、TypeScript 内置工具类
更多请查看 node_modules/typescript/lib/lib.es5.d.ts
14.1 Partial
将一个类型的属性全部变为可选
type Partial<T> = {
[P in keyof T]?: T[P];
};
interface User {
name: string;
sex: string;
age: number;
}
type NewType = Partial<User>;
// 结果
type NewType = {
name?: string | undefined;
sex?: string | undefined;
age?: number | undefined;
}
14.2 Required
将一个类型的属性全部变为必选
type Required<T> = {
[P in keyof T]-?: T[P];
};
// - 表示取反 -? 表示取消可选
interface User {
name?: string;
sex?: string;
age?: number;
}
type NewType = Required<User>;
// 结果
type NewType = {
name: string;
sex: string;
age: number;
}
14.3 Readonly
将一个类型的属性全部变为只读状态
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
interface User {
name: string;
sex: string;
age: number;
}
type NewType = Readonly<User>;
// 结果
type NewType = {
readonly name: string;
readonly sex: string;
readonly age: number;
}
14.4 Pick
从一个 Type 中选取一些属性来构造一个新的对象 Type
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
interface User {
name: string;
sex: string;
age: number;
}
type NewType = Pick<User, 'name' | 'sex'>;
// 结果
type NewType = {
name: string;
sex: string;
}
14.5 Exclude
排除一个联合类型中的某一些类型来构造一个新 Type
type Exclude<T, U> = T extends U ? never : T;
interface User {
name: string;
sex: string;
age: number;
}
type NewType = Exclude<keyof User, 'name' | 'sex'>;
// 结果
type NewType = "age"
14.6 Extract
提取出一个联合类型中的某一些类型来构造一个新 Type
type Extract<T, U> = T extends U ? T : never;
interface User {
name: string;
sex: string;
age: number;
}
type NewType = Extract<keyof User, 'name' | 'sex'>;
// 结果
type NewType = "name" | "sex";
14.7 Omit
从一个对象类型中删除一些属性来构造一个新的对象 Type
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
interface User {
name: string;
sex: string;
age: number;
}
type NewType = Omit<User, 'name' | 'sex'>;
// 结果
type NewType = {
age: number;
}
14.8 Record
构造一个字面量对象 Type
type Record<K extends keyof any, T> = {
[P in K]: T;
};
interface User {
name: string;
sex: string;
age: number;
}
type NewType = Record<string, User>;
// 结果
type NewType = {
[x: string]: User;
}
type record1 = Record<string, any> // { [x: string]: any; } 索引签名
type record2 = Record<'a' | 'b', 1> // { a: 1, b: 1 }
14.9 NonNullable
从类型中排除 null 和 undefined 来构造一个新的 Type
type NonNullable<T> = T extends null | undefined ? never : T;
type name = string | null | undefined;
type newType = NonNullable<name>;
// 结果
type newType = string
14.10 ReturnType
用函数 Type 的返回值定义一个新的 Type
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
interface User {
name: string;
sex: string;
age: number;
getFamily: (name: string) => User[];
}
type NewType = ReturnType<User['getFamily']>;
// 结果
type NewType = User[];
14.11 Parameters
从 [函数 Type] 的形参构造一个数组 Type
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
interface User {
name: string;
sex: string;
age: number;
getFamily: (name: string) => User[];
}
type NewType = Parameters<User['getFamily']>;
// 结果
type NewType = [name: string];
14.12 ConstructorParameters
从定义的[构造函数]的形参构造数组 Type
type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;
type NewType = ConstructorParameters<new (name: string) => { name: string; age: number }>;
// 结果
type NewType = [name: string]
14.13 InstanceType
从一个构造函数的实例定义一个新的 Type
type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any;
type NewType = InstanceType<new (name: string) => { name: string; age: number }>;
// 结果
type NewType = {
name: string;
age: number;
}
14.14 ThisParameterType
提取函数 Type 的 this 参数生成一个新的 Type
type ThisParameterType<T> = T extends (this: infer U, ...args: any[]) => any ? U : unknown;
function fn(this: { name: string }) {}
const getThisType: ThisParameterType<typeof fn> = { name: 'ts' };
// { name: string }
14.15 OmitThisParameter
忽略函数 Type 的 this 参数,生成一个新的函数 Type
type OmitThisParameter<T> = unknown extends ThisParameterType<T> ? T : T extends (...args: infer A) => infer R ? (...args: A) => R : T;
function fn(this: { name: string }, id: string) {};
const nonThisType: OmitThisParameter<typeof fn> = (id: string) => {};
// (id: string) => void
14.16 OmitThisParameter
给对象标记 this 接口; 这个类型在 lib.d.ts 中定义的就是一个{}空标签;noImplicitThis 规则开启后在函数中的this在不定义的情况下不能使用,相当于严格模式,默认情况下noImplicitThis的值为false,除非手动开启,否则ThisType毫无作用
14.17 Uppercase
将字符串中的每个字符转换为大写
type Uppercase<S extends string> = intrinsic;
type Str = "Ts"
type R = Uppercase<Str>; // TS
14.18 Lowercase
将字符串中的每个字符转换为小写
type Lowercase<S extends string> = intrinsic;
type Str = "TS"
type R = Lowercase<Str>; // ts
14.19 Capitalize
将字符串中的第一个字符转换为大写
type Capitalize<S extends string> = intrinsic;
type Str = "ts"
type R = Capitalize<Str>; // Ts
14.20 Uncapitalize
将字符串中的第一个字符转换为小写
type Uncapitalize<S extends string> = intrinsic;
type Str = "TS"
type R = Uncapitalize<Str>; // tS
14.21 Awaited
TypeScript 4.5
type Awaited<T> = T extends null | undefined ? T : T extends object & {
then(onfulfilled: infer F): any;
} ? F extends (value: infer V, ...args: any) => any ? Awaited<V> : never : T
十五、编译上下文
15.1 tsconfig.json 的作用
-
命令行直接输入 tsc 命令不带任何参数进行编译:
此时编译器会从当前目录开始查找 tsconfig.json 文件,如果当前目录没有发现该文件,则逐级向父级目录搜索。如果一直没有检索到该文件,编译器会给出使用提示。
-
命令行调用 tsc 带参数 --project(或 -p) 而指定一个目录:
编译器直接在该目录下查找 tsconfig.json 文件,如果没找到则报错。
-
命令行调用 tsc 后直接指定文件:
直接编译指定的文件。
15.2 tsconfig.json 重要字段
-
files - 数组类型,用于表示由 ts 管理的 文件 的具体路径,可以是相对或绝对路径,不支持 glob 匹配模式的路径;
-
include - 数组类型,设置需要进行编译的文件,支持路径模式正则匹配,支持的glob通配符;
-
exclude - 数组类型,设置无需进行编译的文件,支持路径模式正则匹配,支持的glob通配符;
*匹配0或多个字符(不包括目录分隔符)表示任意文件;?匹配一个任意字符(不包括目录分隔符);**/递归匹配任意子目录,表示任意目录
注意:优先级:files > exclude > include 。如果不指定 files ,项目目录下的所有文件都会被编译器编译。如果同一个文件在三者中均指定,此文件一定会被编译器编译。files 中不指定而在 exclude、include 中同时指定的文件也会被编译,因为优先级是这样的 exclude > include 。另外,exclude默认情况下会排除node_modules,bower_components,jspm_packages 和 outDir 目录;
-
compileOnSave - 布尔类型,可以让 IDE 在保存文件的时候根据 tsconfig.json 重新生成编译后的文件;
-
extends - 字符串类型,该值是一个路径,指定另一个配置文件用于继承 tsconfig.json 中的配置。在原文件里的配置最先被加载,原文件里的配置被继承文件里的同名配置所重写。 如果发现循环引用,则会报错;
-
typeAcquisition - 对象类型,设置自动引入库类型定义文件;
-
watchOptions - 对象类型,typescript3.8 以上新增加的配置,用来配置使用哪种监听策略来跟踪文件和目录;
-
reference - 支持将 TypeScript 程序的结构分割成更小的组成部分;
-
compilerOptions - 设置与编译流程相关的选项;
15.3 compilerOptions 选项
compilerOptions 支持很多选项,常见的有 baseUrl、 target、baseUrl、 moduleResolution 和 lib 等。
compilerOptions 每个选项的详细说明如下:
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Projects */
// "incremental": true, /* 启用增量编译 */
// "composite": true, /* 启用允许将类型脚脚本项目与项目引用一起使用的约束 */
// "tsBuildInfoFile": "./", /* 指定.tsbuildinfo增量编译文件的文件夹 */
// "disableSourceOfProjectReferenceRedirect": true, /* 在引用复合项目时,将禁用首选的源文件,而不是声明文件 */
// "disableSolutionSearching": true, /* 在编辑时,选择一个项目退出多项目引用检查 */
// "disableReferencedProjectLoad": true, /* 减少通过类型脚本自动加载的项目数量 */
/* Language and Environment */
"target": "es2016", /* 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT' */
// "lib": [], /* 指定要包含在编译中的库文件 */
// "jsx": "preserve", /* 指定生成的JSX代码: 'preserve', 'react-native', or 'react' */
// "experimentalDecorators": true, /* 启用装饰器 */
// "emitDecoratorMetadata": true, /* 为装饰器提供元数据的支持 */
// "jsxFactory": "", /* 指定针对ReactJSX发射时使用的JSX工厂函数,例如“React.createElement”或“h” */
// "jsxFragmentFactory": "", /* 指定用于片段的JSX片段引用例如:React.Fragment' or 'Fragment */
// "jsxImportSource": "", /* 指定用于在使用`jsx时导入JSX工厂函数的模块说明符:反应-jsx*`.` */
// "reactNamespace": "", /* 指定为`创建元素`调用的对象。这只适用于针对`,`JSX emit */
// "noLib": true, /* 禁用包括任何库文件,包括默认的lib.d.ts */
// "useDefineForClassFields": true, /* emit ECMAScript-符合标准的类字段 */
/* Modules */
"module": "commonjs", /* 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015' */
// "rootDir": "./", /* 在源文件中指定根文件夹,用来控制输出目录结构 --outDir */
// "moduleResolution": "node", /* 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6) */
// "baseUrl": "./", /* 用于解析非相对模块名称的基目录 */
// "paths": {}, /* 模块名到基于 baseUrl 的路径映射的列表 */
// "rootDirs": [], /* 根文件夹列表,其组合内容表示项目运行时的结构内容 */
// "typeRoots": [], /* 包含类型声明的文件列表 */
// "types": [], /* 需要包含的类型声明文件名列表 */
// "allowUmdGlobalAccess": true, /* 允许从模块访问UMD全局文件 */
// "resolveJsonModule": true, /* 启用导入.json文件 */
// "noResolve": true, /* 不允许`import`,`require`或`reference`来扩展类型应该添加到项目的文件数量 */
/* JavaScript Support */
// "allowJs": true, /* 允许编译 javascript 文件 */
// "checkJs": true, /* 报告 javascript 文件中的错误 */
// "maxNodeModuleJsDepth": 1, /* 指定用于从`node_modules`中检查JavaScript文件的最大文件夹深度。仅适用于`允许的Js` */
/* Emit */
// "declaration": true, /* 从项目中的typeScript和JavaScript文件生成相应的.d.ts文件 */
// "declarationMap": true, /* 为d.ts文件创建源集映射 */
// "emitDeclarationOnly": true, /* 只输出d.ts文件,而不输出JavaScript文件 */
// "sourceMap": true, /* 生成相应的 '.map' 映射文件 */
// "outFile": "./", /* 将输出文件合并为一个文件 */
// "outDir": "./", /* 指定输出目录 */
// "removeComments": true, /* 删除编译后的所有的注释 */
// "noEmit": true, /* 不生成输出文件 */
// "importHelpers": true, /* 从 tslib 导入辅助工具函数 */
// "importsNotUsedAsValues": "remove", /* 为仅用于类型的导入指定发射/检查行为 */
// "downlevelIteration": true, /* Emit 更兼容,但冗长和性能较差的JavaScript的迭代 */
// "sourceRoot": "", /* 指定调试器应该找到 TypeScript 文件而不是源文件的位置 */
// "mapRoot": "", /* 指定调试器应该找到映射文件而不是生成文件的位置 */
// "inlineSourceMap": true, /* 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件 */
// "inlineSources": true, /* 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性 */
// "emitBOM": true, /* 在输出文件的开头发出UTF-8字节顺序标记(BOM) */
// "newLine": "crlf", /* 设置发射文件的换行符 */
// "stripInternal": true, /* 禁用在其JSDoc注释中具有`@internal`的发射声明 */
// "noEmitHelpers": true, /* 禁用在编译输出中生成自定义助手函数 */
// "noEmitOnError": true, /* 如果报告了任何类型检查错误,则禁用发射文件 */
// "preserveConstEnums": true, /* 禁用擦除生成的代码中的`常数枚举`声明 */
// "declarationDir": "./", /* 为生成的声明文件的输出目录 */
// "preserveValueImports": true, /* 在JavaScript输出中保留未使用的导入值,否则将被删除 */
/* Interop Constraints */
// "isolatedModules": true, /* 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似) */
// "allowSyntheticDefaultImports": true, /* 允许从没有设置默认导出的模块中默认导入。 */
"esModuleInterop": true, /* 发出额外的JavaScript,以简化对导入CommonJS模块的支持。这使得`允许合成默认导入`以实现类型兼容性 */
// "preserveSymlinks": true, /* 禁用对符号链接到其实际路径的解析。这与节点中的同一标志相关联 */
"forceConsistentCasingInFileNames": true, /* 确保进口时外壳正确 */
/* Type Checking */
"strict": true, /* 启用严格严格模式类型检查选项 */
// "noImplicitAny": true, /* 在表达式和声明上有隐含的 any类型时报错 */
// "strictNullChecks": true, /* 启用严格`null`和`undefined`检查 */
// "strictFunctionTypes": true, /* 在分配函数时,请检查以确保参数和返回值与子类型兼容 */
// "strictBindCallApply": true, /* 检查`绑定`、`调用`和`应用`方法的参数是否与原始函数匹配 */
// "strictPropertyInitialization": true, /* 检查在构造函数中已声明但未设置的类属性 */
// "noImplicitThis": true, /* 当`this`具有类型`any`时,报错 */
// "useUnknownInCatchVariables": true, /* 将catch子句变量类型化为“unknown”,而不是“any” */
// "alwaysStrict": true, /* 以严格模式检查每个模块,并在每个文件里加入 'use strict' */
// "noUnusedLocals": true, /* 有未使用的变量时,抛出错误 */
// "noUnusedParameters": true, /* 有未使用的参数时,抛出错误 */
// "exactOptionalPropertyTypes": true, /* 将可选的属性类型解释为已写入的,而不是添加“undefined” */
// "noImplicitReturns": true, /* 并不是所有函数里的代码都有返回值时,抛出错误 */
// "noFallthroughCasesInSwitch": true, /* 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿) */
// "noUncheckedIndexedAccess": true, /* 在索引签名结果中包含“undefined” */
// "noImplicitOverride": true, /* 确保在派生类中标记覆盖成员 */
// "noPropertyAccessFromIndexSignature": true, /* 对使用索引类型声明的密钥强制使用索引访问器 */
// "allowUnusedLabels": true, /* 禁用对未使用的标签的错误报告 */
// "allowUnreachableCode": true, /* 禁用对不可达代码的错误报告 */
/* Completeness */
// "skipDefaultLibCheck": true, /* 跳过类型检查。d.ts类型脚本中包含的ts文件 */
"skipLibCheck": true /* 跳过类型检查所有。d.ts文件 */
}
}
十六、TypeScript 开发辅助工具
16.1 TypeScript Playground
TypeScript 官方提供的在线 TypeScript 运行环境,利用它你可以方便地学习 TypeScript 相关知识与不同版本的功能特性。
除了 TypeScript 官方的 Playground 之外,你还可以选择其他的 Playground,比如 codepen.io、stackblitz 或 jsbin.com 等。
16.2 TypeScript UML Playground
一款在线 TypeScript UML 工具,利用它你可以为指定的 TypeScript 代码生成 UML 类图。
16.3 JSON TO TS
一款 TypeScript 在线工具,利用它你可以为指定的 JSON 数据生成对应的 TypeScript 接口定义。
除了使用 jsontots 在线工具之外,对于使用 VSCode IDE 的小伙们还可以安装 JSON to TS 扩展来快速完成 JSON to TS 的转换工作。
16.4 Schemats
利用 Schemats,你可以基于(Postgres,MySQL)SQL 数据库中的 schema 自动生成 TypeScript 接口定义。
16.5 TypeScript AST Viewer
一款 TypeScript AST 在线工具,利用它你可以查看指定 TypeScript 代码对应的 AST(Abstract Syntax Tree)抽象语法树。
对于了解过 AST 的小伙伴来说,对 astexplorer 这款在线工具应该不会陌生。该工具除了支持 JavaScript 之外,还支持 CSS、JSON、RegExp、GraphQL 和 Markdown 等格式的解析。
16.6 TypeDoc
TypeDoc 用于将 TypeScript 源代码中的注释转换为 HTML 文档或 JSON 模型。它可灵活扩展,并支持多种配置。
16.7 TypeScript ESLint
使用 TypeScript ESLint 可以帮助我们规范代码质量,提高团队开发效率。