Typescript安装
- 使用npm全局安装
// 全局安装
npm install -g typescript
// 查看 tsc(typescript compiler) 版本
tsc -v
- 使用tsc命令
// 编译ts文件
tsc fileName.ts
数据类型
Javascript基础数据类型
- Boolean
- Null
- Undefined
- Number
- BigInt
- String
- Symbol
Typescript基础数据类型
Boolean 类型
let isDone: boolean = false
Number 类型
// 对于 number 类型,es6 还支持2进制和8进制,让我们来感受下
let age: number = 10
let binaryNumber: number = 0b1111
String 类型
// 使用es6新增的模版字符串也是没有问题的
let firstName: string = 'viking'
let message: string = `Hello, ${firstName}, age is ${age}`
Array和Tuple
//最简单的方法是使用「类型 + 方括号」来表示数组:
let arrOfNumbers: number[] = [1, 2, 3, 4]
//数组的项中不允许出现其他的类型:
//数组的一些方法的参数也会根据数组在定义时约定的类型进行限制:
arrOfNumbers.push(3)
arrOfNumbers.push('abc')
// 元组的表示和数组非常类似,只不过它将类型写在了里面
// 这就对每一项起到了限定的作用
let user: [string, number] = ['viking', 20]
//但是当写少一项 就会报错 同样写多一项也会有问题
user = ['molly', 20, true] // 提示异常
// 和数组一样,可以使用下标来访问元组中的元素
console.log(user[0]); // viking
console.log(user[1]); // 20
Enum 类型
- 数字枚举
// 数字枚举,一个数字枚举可以用 enum 这个关键词来定义
// 定义一系列的方向,然后这里面的值,默认情况下,枚举成员会被赋值为从 0 开始递增的数字,也可以自定义初始值
enum Direction {
Up = 3,
Down,
Left,
Right,
}
console.log(Direction.Up) // 3
// 还有一个神奇的点是这个枚举还做了反向映射
console.log(Direction[0]) // undefined
console.log(Direction[3]) // Up
- 字符串枚举
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT',
}
const value = 'UP'
if (value === Direction.Up) {
console.log('go up!')
}
Any 和 Unknown 类型
// any类型
// 在知道明确类型后应当使用明确的数据类型,如果都使用any类型,那么Typescript与Javascript就没有区别了
let notSure: any = 4
notSure = 'maybe it is a string'
notSure = 'boolean'
// 在任意值上访问任何属性都是允许的:
notSure.myName
// 也允许调用任何方法:
notSure.getName()
// unknown类型
// 就像所有类型都可以赋值给 any,所有类型也都可以赋值给 unknown。
// 这使得 unknown 成为 TypeScript 类型系统的另一种顶级类型(另一种是 any)
// unknown可以理解为是严格版的any类型
// unknown用法:当想使用any的时候使用unknown
let value: unknown;
value = true; // OK
value = 42; // OK
// unknown 类型只能被赋值给 any 类型和 unknown 类型本身
let value: unknown;
let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error
// 对于unknown类型,无法像any类型一样可以进行任何操作(属性的访问和方法调用)
Void 类型
// 某种程度上来说,void 类型像是与 any 类型相反,它表示没有任何类型。
//当一个函数没有返回值时,你通常会见到其返回值类型是 void
// 声明函数返回值为void
function warnUser(): void {
console.log("This is my warning message");
}
Null 和 Undefined 类型
// undefined 和 null 两者有各自的类型分别为 undefined 和 null
let u: undefined = undefined
let n: null = null
// undefined 和 null 是所有类型的子类型
// 也就是说 undefined 类型的变量,可以赋值给 number 类型的变量
let num: number = undefined
Never 类型
// never 类型通常用来实现代码的全面性检查
// 从以下示例中可以看出:使用 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 分支里面,把 foo 赋值给一个显示声明的 never 变量
// 如果一切逻辑正确,那么这里应该能够编译通过,如果后面更改了 Foo 的类型:
type Foo = string | number | boolean;
// 但是,忘记同时修改 controlFlowAnalysisWithNever 方法中的控制流程
// 这时候 else 分支的 foo 类型会被收窄为 boolean 类型,导致无法赋值给 never 类型,这时就会产生一个编译错误
// 通过这个方式,我们可以确保
controlFlowAnalysisWithNever 方法总是穷尽了 Foo 的所有可能类型
类型的扩展内容
类型断言
有时候会有这样的情况,你会比 TypeScript 更了解某个值的详细信息。通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型
通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。
类型断言好比其他语言里的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用
<> 语法
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;
应用示例
function getLength(input: string | number): number {
const str = input as string
if (str.length) {
return str.length
} else {
const number = input as number
return number.toString().length
}
}
类型守卫
类型守卫检查数据类型的一种表达式,检查时通常使用一些关键字来进行类型判断,更多内容可参照下文中 泛型工具类型中的前置基础知识
in 关键字
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);
}
}
printEmployeeInformation({name: 'name', privileges: ['1', '3']})
// Name: admin
// Privileges: 1,3
printEmployeeInformation({name: 'name', startDate: new Date()})
// Name: employee
// Start Date: Thu Apr 22 2021 10:52:29 GMT+0800 (中国标准时间)
typeof 关键字
- typeof 类型保护只支持两种形式:typeof v === "typename" 和 typeof v !== typename
- "typename" 的标准字符串必须是 "number", "string", "boolean" 或 "symbol",Typescript也允许与其他的字符串进行比较
function getLength2(input: string | number): number {
if (typeof input === 'string') {
return input.length
} else {
return input.toString().length
}
}
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);
console.log('padder instanceof SpaceRepeatingPadder === ' + (padder instanceof SpaceRepeatingPadder))
// padder instanceof SpaceRepeatingPadder === true
is
用于类型指定,使用示例
// 当isError返回的结果为true时,会将传入的value直接转换为Error类型,并且在整个对应的代码块中有效,如条件判断语句块中有效
const isError = (value: any): value is Error => value?.message
联合类型和类型别名
联合类型
- 使用中竖线来分割多个类型
let numberOrString: number | string
// 联合类型通常与 null 或 undefined 一起使用
const sayHello = (name: string | undefined) => {
/* ... */
}
// 当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候
// 只能访问此联合类型的所有类型里共有的属性或方法:
numberOrString.length
numberOrString.toString()
类型别名
- 类型别名,就是给类型起一个别名,让它可以更方便的被重用
let sum: (x: number, y: number) => number
const result = sum(1,2)
type PlusType = (x: number, y: number) => number
let sum2: PlusType
// 支持联合类型
type StrOrNumber = string | number
let result2: StrOrNumber = '123'
result2 = 123
// 字符串字面量
type Directions = 'Up' | 'Down' | 'Left' | 'Right'
let toWhere: Directions = 'Up'
// 限定类型的取值(只支持原始数据类型)
// 限定str的类型为字符串类型,并且其取值只能为name,等于其他值时会报错
const str: 'name' = 'name'
// 限定number的类型只能为数字1,为其他值时会报错
const number: 1 = 1
交叉类型
// 示例1
interface IName {
name: string
}
type IPerson = IName & { age: number }
let person: IPerson = { name: 'hello', age: 12}
// 示例2
interface IPerson {
id: string;
age: number;
}
interface IWorker {
companyId: string;
}
type IStaff = IPerson & IWorker;
const staff: IStaff = {
id: 'E1006',
age: 33,
companyId: 'EFT'
};
console.log(staff)
数组的特殊运算
数组解构
let x: number; let y: number; let z: number;
let five_array = [0,1,2,3,4];
[x,y,z] = five_array;
console.log(x,y,z); // 0, 1, 2
数组展开运算符
let two_array = [0, 1];
let five_array = [...two_array, 2, 3, 4];
console.log(five_array); // [0, 1, 2, 3, 4]
数组遍历
let colors: string[] = ["red", "green", "blue"];
for (let i of colors) {
console.log(i);
}
// red
// green
// blue
对象的特殊运算
对象解构
let person = {
nickname: "Semlinker",
gender: "Male",
};
let { nickname, gender } = person;
console.log(nickname, gender); // Semlinker, Male
对象展开运算符
let person = {
nickname: "Semlinker",
gender: "Male",
address: "Xiamen",
};
// 组装对象
let personWithAge = { ...person, age: 33 };
console.log(personWithAge);
// {
// "nickname": "Semlinker",
// "gender": "Male",
// "address": "Xiamen",
// "age": 33
// }
// 获取除了某些项外的其它项
let { nickname, ...rest } = person;
console.log(nickname, rest);
// "Semlinker", {
// "gender": "Male",
// "address": "Xiamen"
// }
其他高级类型
面向对象编程
- 面向对象编程的三大特点
- 封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象
- 继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性
- 多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应
Function类型
- TypeScript 函数与 JavaScript 函数的区别
TypeScript | JavaScript |
---|---|
含有类型 | 无类型 |
箭头函数 | 箭头函数(ES2015) |
函数类型 | 无函数类型 |
必填和可选参数 | 所有参数都是可选的 |
默认参数 | 默认参数 |
剩余参数 | 剩余参数 |
函数重载 | 无函数重载 |
- 箭头函数
myBooks.forEach(() => console.log('reading'));
myBooks.forEach(title => console.log(title));
myBooks.forEach((title, idx, arr) =>
console.log(idx + '-' + title);
);
myBooks.forEach((title, idx, arr) => {
console.log(idx + '-' + title);
});
- 参数类型和返回类型
// 约定输入,约定输出
function add(x: number, y: number): number {
return x + y
}
// 可选参数
// 注意:可选参数后面不能再增加必选的参数
function add(x: number, y: number, z?: number): number {
if (typeof z === 'number') {
return x + y + z
} else {
return x + y
}
}
- 函数类型
let IdGenerator: (chars: string, nums: number) => string;
// interface 描述函数类型
const sum = (x: number, y: number) => {
return x + y
}
interface ISum {
(x: number, y: number): number
}
const sum2: ISum = sum
- 可选参数及默认参数
// 可选参数
function createUserId(name: string, id: number, age?: number): string {
return name + id;
}
const add2: (x: number, y: number, z?:number) => number = add
// 由于add函数的第三个参数为可选参数,因此以下两种赋值方法也是允许的
const add1: (x: number, y: number, z: number) => number = add
const add2: (x:number, y: number) => number = add
// 以下方法将会报错
const add3: (x: number) => number = add
// 默认参数
function createUserId(
name: string = "Semlinker",
id: number,
age?: number
): string {
return name + id;
}
- 剩余参数
function push(array, ...items) {
items.forEach(function (item) {
array.push(item);
});
}
let a = [];
push(a, 1, 2, 3);
- 函数重载
方法重载是指在同一个类中方法同名,参数不同(参数类型不同、参数个数不同或参数个数相同时参数的先后顺序不同),调用时根据实参的形式,选择与它匹配的方法执行操作的一种技术,从定义中可以看出方法重载的两个条件为:
- 在同一个类中
- 方法名相同且参数列表不同
type Combinable = number | string
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("Semlinker", " Kakuqo");
// 注意:当 TypeScript 编译器处理函数重载时,会查找重载列表,尝试使用第一个重载定义。如果匹配的话就使用这个
// 因此,在定义重载的时候,一定要把最精确的定义放在最前面
Interface接口
- 在面向对象语言中,接口是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类去实现
- TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述
// 定义一个接口 Person
interface Person {
name: string;
age: number;
}
// 接着定义了一个变量 viking,它的类型是 Person,这样就约束了 viking 的形状必须和接口 Person 一致。
let viking: Person ={
name: 'viking',
age: 20
}
//有时希望不要完全匹配一个形状,那么可以用可选属性:
interface Person {
name: string;
age?: number;
}
let viking: Person = {
name: 'Viking'
}
//接下来还有只读属性,有时候希望对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly 定义只读属性
interface Person {
readonly id: number;
name: string;
age?: number;
}
viking.id = 9527 // 提示错误
// 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!
Class类
-
类成员的访问修饰符
- public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的
- private 修饰的属性或方法是私有的,不能在声明它的类的外部访问
- protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的
-
类的属性和方法
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");
编译后的js代码如下
"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");
- 访问器
在 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 = "Semlinker";
if (employee.fullName) {
console.log(employee.fullName);
}
- 类的继承
继承 (Inheritance) 是一种联结类与类的层次模型。指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系
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 {
// 这里重写构造函数,注意在子类的构造函数中,必须使用 super 调用父类的方法,否则就会报错
constructor(name: string) {
super(name);
}
move(distanceInMeters = 5) {
console.log("Slithering...");
super.move(distanceInMeters);
}
}
let sam = new Snake("Sammy the Python");
sam.move();
- ECMAScript 私有字段
在 TypeScript 3.8 版本就开始支持ECMAScript 私有字段,与常规属性》(甚至使用 private 修饰符声明的属性)不同,私有字段要牢记以下规则:
- 私有字段以 # 字符开头,有时我们称之为私有名称;
- 每个私有字段名称都唯一地限定于其包含的类;
- 不能在私有字段上使用 TypeScript 可访问性修饰符(如 public 或 private);
- 私有字段不能在包含的类之外访问,甚至不能被检测到
class Person {
#name: string;
constructor(name: string) {
this.#name = name;
}
greet() {
console.log(`Hello, my name is ${this.#name}!`);
}
}
let semlinker = new Person("Semlinker");
semlinker.#name;
- 类与接口
interface Radio {
switchRadio(trigger: boolean): void;
}
class Car implements Radio {
switchRadio(trigger) {
return 123
}
}
class Cellphone implements Radio {
switchRadio() {
}
}
interface Battery {
checkBatteryStatus(): void;
}
// 要实现多个接口,只需要中间用 逗号 隔开即可。
class Cellphone implements Radio, Battery {
switchRadio() {
}
checkBatteryStatus() {
}
}
Generics泛型
- 泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性
- 泛型(Generics)是允许同一个函数接受不同类型参数的一种模板。相比于使用 any 类型,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类型
function echo(arg) {
return arg
}
const result = echo(123)
// 这时候发现一个问题,当传入了数字时返回了 any 类型
// 声明一个泛型函数
function echo<T>(arg: T): T {
return arg
}
// 在使用时传入具体的类型
const result = echo(123)
// 泛型也可以传入多个值
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]]
}
const result = swap(['string', 123])
- 泛型与类和接口
// 示例1
class Queue {
private data = [];
push(item) {
return this.data.push(item)
}
pop() {
return this.data.shift()
}
}
const queue = new Queue()
queue.push(1)
queue.push('str')
console.log(queue.pop().toFixed())
console.log(queue.pop().toFixed())
// 在上述代码中存在一个问题,它允许向队列中添加任何类型的数据,当然,当数据被弹出队列时,也可以是任意类型。
// 在上面的示例中,看起来可以向队列中添加string 类型的数据,但是在使用的过程中,就会出现无法捕捉到的错误
class Queue<T> {
private data = [];
push(item: T) {
return this.data.push(item)
}
pop(): T {
return this.data.shift()
}
}
const queue = new Queue<number>()
//泛型和 interface
interface KeyPair<T, U> {
key: T;
value: U;
}
let kp1: KeyPair<number, string> = { key: 1, value: "str"}
let kp2: KeyPair<string, number> = { key: "str", value: 123}
// 示例2
// 在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法
function echoWithArr<T>(arg: T): T {
console.log(arg.length)
return arg
}
// 上例中在编辑时即会报错,原因是泛型 T 不一定包含属性 length
// 可以传入任意类型,当然有些不包括 length 属性,那样就会报错,可以使用下面的方式进行改进
function echoWithArr<T>(arg: T[]): T[] {
console.log(arg.length)
return arg
}
// 除上述改进方法之外,还可以使用泛型接口的方式进行改进
interface IWithLength {
length: number;
}
function echoWithLength<T extends IWithLength>(arg: T): T {
console.log(arg.length)
return arg
}
echoWithLength('str')
const result3 = echoWithLength({length: 10})
const result4 = echoWithLength([1, 2, 3])
- 常见泛型变量
- T(Type):表示一个 TypeScript 类型
- K(Key):表示对象中的键类型
- V(Value):表示对象中的值类型
- E(Element):表示元素类型
泛型工具类型
前置基础知识
- typeof
在 TypeScript 中,typeof 操作符可以用来获取一个变量声明或对象的类型
interface Person {
name: string;
age: number;
}
const sem: Person = { name: 'semlinker', age: 30 };
type Sem= typeof sem; // Person
function toArray(x: number): Array<number> {
return [x];
}
type Func = typeof toArray; // (x: number) => number[]
- keyof(索引类型查询操作符)
keyof 操作符可以用来索引一个对象中的所有 key 值
interface Person {
name: string;
age: number;
}
type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join"
type K3 = keyof { [x: string]: Person }; // string | number
- in
in 用来遍历枚举类型
type Keys = "a" | "b" | "c"
type Obj = {
[p in Keys]: any
} // -> { a: any, b: any, c: any }
- infer
在 extends 条件类型语句中,可以用 infer 声明一个类型变量并且对它进行使用,在开发中infer主要用于提取一些数据的类型
// 提取函数的参数类型
// 整句表示的意思为:如果 T 能赋值给 (...args: infer P) => any,则结果是 (...args: infer P) => any 类型中的参数 P,否则返回为 T
type ParamType<T> = T extends (...args: infer P) => any ? P : T;
// 提取函数的返回类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
工具类型
- Partial
Partial
的作用就是将某个类型里的属性全部变为可选项 ?
// 定义
type Partial<T> = {
[P in keyof T]?: T[P];
};
// 在以上代码中,首先通过 keyof T 拿到 T 的所有属性名,然后使用 in 进行遍历,将值赋给 P,最后通过 T[P] 取得相应的属性值。中间的 ? 号,用于将所有属性变为可选
// 使用示例
interface Todo {
title: string;
description: string;
}
// 在此函数中,通过Partial<Todo>将Todo的属性变为可选属性,在todo2调用时不会出错
function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
return { ...todo, ...fieldsToUpdate };
}
const todo1 = {
title: "organize desk",
description: "clear clutter",
};
const todo2 = updateTodo(todo1, {
description: "throw out trash",
});
- Required
- Required
的作用是将某个类型中的可选的属性变成必选
type Required<T> = {
[P in keyof T]-?: T[P];
}
// 通过 -? 移除了可选属性中的 ?,使得属性从可选变为必选的
内置类型
const a: Array<number> = [1,2,3]
// Array这个类型,不同的文件中有多处定义,但是它们都是内部定义的一部分,然后根据不同的版本或者功能合并在了一起
// 一个interface 或者 类多次定义会合并在一起。这些文件一般都是以 lib 开头,以 d.ts 结尾,表示这是一个内置对象类型
const date: Date = new Date()
const reg = /abc/
// 还有一些 build in object,内置对象,比如 Math
// 与其他全局对象不同的是,Math 不是一个构造器。Math 的所有属性与方法都是静态的
Math.pow(2,2)
// DOM 和 BOM 标准对象
// document 对象,返回的是一个 HTMLElement
let body: HTMLElement = document.body
// document 上面的query 方法,返回的是一个 nodeList 类型
let allLis = document.querySelectorAll('li')
// 当然添加事件也是很重要的一部分,document 上面有 addEventListener 方法
// 注意这个回调函数,因为类型推断,这里面的 e 事件对象也自动获得了类型,这里是个 mouseEvent 类型
// 因为点击是一个鼠标事件,现在就可以方便的使用 e 上面的方法和属性
document.addEventListener('click', (e) => {
e.preventDefault()
})
TypeScript 装饰器
装饰器介绍
- 装饰器是什么
- 它是一个表达式,对该表达式求值后,返回一个函数(即表达式是一个函数或者使用()执行后返回一个函数)
- 装饰器的分类
- 类装饰器(Class decorators)
- 属性装饰器(Property decorators)
- 方法装饰器(Method decorators)
- 参数装饰器(Parameter decorators)
- 访问器装饰器(getter/setter)(Accessor decorators)
function thinFace() {
console.log('开启瘦脸')
}
function IncreasingEyes() {
console.log('增大眼睛')
}
@thinFace
@IncreasingEyes
class Girl {
}
// 运行结果
增大眼睛
开启瘦脸
- 装饰器工厂
- 有时需要给装饰器传递一些参数,这要借助于装饰器工厂函数。装饰器工厂函数实际上就是一个高阶函数,在调用后返回一个函数,返回的函数作为装饰器函数
function thinFace(value: string){
console.log('1-瘦脸工厂方法')
return function(){
console.log(`4-我是瘦脸的装饰器,要瘦脸${value}`)
}
}
function IncreasingEyes(value: string) {
console.log('2-增大眼睛工厂方法')
return function(){
console.log(`3-我是增大眼睛的装饰器,要${value}`)
}
}
@thinFace('50%')
@IncreasingEyes('增大一倍')
class Girl {
}
// 运行结果
1-瘦脸工厂方法
2-增大眼睛工厂方法
3-我是增大眼睛的装饰器,要增大一倍
4-我是瘦脸的装饰器,要瘦脸50%
- 当多个装饰器应用在一个声明上时的执行顺序
- 由上至下依次对装饰器表达式求值(注意此处的求值是获取装饰器函数,如果装饰器是一个装饰器工厂,则会执行装饰器工厂并返回装饰器函数,如果已经是一个装饰器函数,则不会执行)
- 运行装饰器函数,由下至上依次调用
- 装饰器的执行时机
- 装饰器对类的行为的改变,是代码编译时发生的(不是TypeScript编译,而是js在执行机中编译阶段),而不是在运行时。这意味着,装饰器能在编译阶段运行代码。也就是说,装饰器本质就是编译时执行的函数
- 类中不同声明上的装饰器的应用顺序
- 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员
- 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个静态成员
- 参数装饰器应用到构造函数
- 类装饰器应用到类
- 实例成员:实例成员本身的装饰器 -> 参数装饰器(多个参数时执行顺序为从后往前) -> 方法/访问器/属性装饰器(这三者的执行顺序取决于声明它们的顺序)
- 静态成员:静态成员本身的装饰器 -> 参数装饰器(多个参数时执行顺序为从后往前) -> 方法/访问器/属性装饰器(这三者的执行顺序取决于声明它们的顺序)
- 构造器:参数装饰器
- 类装饰器
- 重要参考:装饰者模式和TypeScript装饰器
开启对装饰器的支持
- 命令行编译文件时开启对装饰器的支持
tsc --target ES5 --experimentalDecorators test.ts
- 配置 tsconfig.json 开启对装饰器的支持
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
装饰器概览
类装饰器 | 方法装饰器 | 访问器装饰器 | 属性装饰器 | 参数装饰器 | |
---|---|---|---|---|---|
使用方式 | @foo class Bar {} | @foo public bar() {} | @foo get bar() | @foo() bar: number | bar(@foo para: string) {} |
传入参数 | constructor | target, propertyKey, descriptor | target, propertyKey, descriptor | target, propertyKey | target, propertyKey, parameterIndex |
- 参数释义
- constructor: 类的构造函数
- target: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
- propertyKey: 成员的名字
- descriptor: 成员的属性描述符
- parameterIndex: 参数在函数参数列表中的索引
类装饰器
- 类装饰器应用于类构造函数,可以用来监视,修改或替换类定义
- 类装饰器表达式会在运行时当作函数被调用,如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明
declare type ClassDecorator = <TFunction extends Function>(
target: TFunction
) => TFunction | void;
// 类装饰器参数
// - target: TFunction - 被装饰的类的构造函数
- 使用示例
// 示例一
function Greeter(target: Function): void {
target.prototype.greet = function (): void {
console.log("Hello Semlinker!");
};
}
@Greeter
class Greeting {
constructor() {
// 内部实现
}
}
let myGreeting = new Greeting();
myGreeting.greet(); // Hello Semlinker!
// 示例二
// 自定义输出
function Greeter(greeting: string) {
return function (target: Function) {
target.prototype.greet = function (): void {
console.log(greeting);
};
};
}
@Greeter("Hello TS!")
class Greeting {
constructor() {
// 内部实现
}
}
let myGreeting = new Greeting();
myGreeting.greet(); // Hello TS!
方法装饰器
- 它会被应用到方法的属性描述符上,可以用来监视,修改或者替换方法定义
- 如果方法装饰器返回一个值,它会被用作方法的属性描述符
- 如果代码输出目标版本小于ES5,属性描述符将会是undefined
- 如果代码输出目标版本小于ES5返回值会被忽略
declare type MethodDecorator = <T>(target:Object, propertyKey: string | symbol,
descriptor: TypePropertyDescript<T>) => TypedPropertyDescriptor<T> | void;
// 方法装饰器参数:
// - target: Object - 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
// - propertyKey: string | symbol - 成员的名字
// - descriptor: TypePropertyDescript - 成员的属性描述符
- 使用示例
function LogOutput(tarage: Function, key: string, descriptor: any) {
let originalMethod = descriptor.value;
let newMethod = function(...args: any[]): any {
let result: any = originalMethod.apply(this, args);
if(!this.loggedOutput) {
this.loggedOutput = new Array<any>();
}
this.loggedOutput.push({
method: key,
parameters: args,
output: result,
timestamp: new Date()
});
return result;
};
descriptor.value = newMethod;
}
class Calculator {
@LogOutput
double (num: number): number {
return num * 2;
}
}
let calc = new Calculator();
calc.double(11);
console.log(calc.loggedOutput);
// [{method: "double", output: 22, ...}]
访问器装饰器
- 访问器装饰器应用于访问器的属性描述符并且可以用来监视,修改或替换一个访问器的定义
- 访问器装饰器表达式会在运行时当作函数被调用
- 如果代码输出目标版本小于ES5,属性描述符将会是undefined
- 如果代码输出目标版本小于ES5返回值会被忽略
// 访问器装饰器参数:
// - target: Object - 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
// - propertyKey: string | symbol - 成员的名字
// - descriptor: TypePropertyDescript - 成员的属性描述符
属性装饰器
- 属性装饰器表达式会在运行时当作函数被调用
declare type PropertyDecorator = (target:Object,
propertyKey: string | symbol ) => void;
// 属性装饰器顾名思义,用来装饰类的属性。它接收两个参数:
// - target: Object - 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
// - propertyKey: string | symbol - 成员的名字
- 使用示例
function logProperty(target: any, key: string) {
delete target[key];
const backingField = "_" + key;
Object.defineProperty(target, backingField, {
writable: true,
enumerable: true,
configurable: true
});
// property getter
const getter = function (this: any) {
const currVal = this[backingField];
console.log(`Get: ${key} => ${currVal}`);
return currVal;
};
// property setter
const setter = function (this: any, newVal: any) {
console.log(`Set: ${key} => ${newVal}`);
this[backingField] = newVal;
};
// Create new property with getter and setter
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
class Person {
@logProperty
public name: string;
constructor(name : string) {
this.name = name;
}
}
const p1 = new Person("semlinker");
p1.name = "kakuqo";
// 输出结果
// Set: name => semlinker
// Set: name => kakuqo
参数装饰器
- 参数装饰器应用于类构造函数或方法声明
- 参数装饰器表达式会在运行时当作函数被调用
- 参数装饰器只能用来监视一个方法的参数是否被传入
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol,
parameterIndex: number ) => void
// 参数装饰器参数:
// - target: Object - 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
// - propertyKey: string | symbol - 成员的名字
// - parameterIndex: number - 参数在函数参数列表中的索引
- 使用示例
function Log(target: Function, key: string, parameterIndex: number) {
let functionLogged = key || target.prototype.constructor.name;
console.log(`The parameter in position ${parameterIndex} at ${functionLogged} has
been decorated`);
}
class Greeter {
greeting: string;
constructor(@Log phrase: string) {
this.greeting = phrase;
}
}
// The parameter in position 0 at Greeter has
been decorated
其他语法特性
可选链
- 可选链的核心是允许我们写下如果碰到 null 或者 undefined,TypeScript 能立即停止运行的代码。可选链耀眼的部分是使用 ?. 运算符来访问一个可选属性的运算符,其支持以下语法
obj?.prop
obj?.[expr]
arr?.[index]
func?.(args)
,例如
let x = foo?.bar.baz();
// 等效代码
let x = foo === null || foo === undefined ? undefined : foo.bar.baz();
- ?. 可以用来替代很多使用 && 执行空检查的代码
// Before
if (foo && foo.bar && foo.bar.baz) {
// ...
}
// After-ish
if (foo?.bar?.baz) {
// ...
}
注意:?. 与 && 运算符行为略有不同,&& 专门用于检测 falsy 值,比如空字符串、0、NaN、null 和 false 等。而 ?. 只会验证对象是否为 null 或 undefined,对于 0 或空字符串来说,并不会出现 “短路”
- 可选链的「短路运算」行为被局限在属性的访问、调用以及元素的访问,它不会沿伸到后续的表达式中,例如
let result = foo?.bar / someComputation()
// 可选链不会阻止除法运算或者 someComputation() 调用,它等价于:
let temp = foo === null || foo === undefined ? undefined : foo.bar;
let result = temp / someComputation();
Nullish Coalescing(??)
- 当左侧操作数为 null 或 undefined 时,其返回右侧的操作数,否则返回左侧的操作数,类似于 || ,例如
let x = foo ?? bar();
// 代码等价于:
let x = foo !== null && foo !== undefined ? foo : bar();
- 与 || 不同的是,?? 运算符不会将 0、NaN 以及'' 认为是 false
编译上下文
tsconfig.json 的作用
- 用于标识 TypeScript 项目的根路径;
- 用于配置 TypeScript 编译器;
- 用于指定编译的文件
tsconfig.json 重要字段
- files - 设置要编译的文件的名称;
- include - 设置需要进行编译的文件,支持路径模式匹配;
- exclude - 设置无需进行编译的文件,支持路径模式匹配;
- compilerOptions - 设置与编译流程相关的选项
compilerOptions 选项
- compilerOptions 支持很多选项,常见的有 baseUrl、 target、baseUrl、 moduleResolution 和 lib 等
compilerOptions 每个选项的详细说明如下
{
"compilerOptions": {
/* 基本选项 */
"target": "es5", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
"module": "commonjs", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
"lib": [], // 指定要包含在编译中的库文件
"allowJs": true, // 允许编译 javascript 文件
"checkJs": true, // 报告 javascript 文件中的错误
"jsx": "preserve", // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
"declaration": true, // 生成相应的 '.d.ts' 文件
"sourceMap": true, // 生成相应的 '.map' 文件
"outFile": "./", // 将输出文件合并为一个文件
"outDir": "./", // 指定输出目录
"rootDir": "./", // 用来控制输出目录结构 --outDir.
"removeComments": true, // 删除编译后的所有的注释
"noEmit": true, // 不生成输出文件
"importHelpers": true, // 从 tslib 导入辅助工具函数
"isolatedModules": true, // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似).
/* 严格的类型检查选项 */
"strict": true, // 启用所有严格类型检查选项
"noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错
"strictNullChecks": true, // 启用严格的 null 检查
"noImplicitThis": true, // 当 this 表达式值为 any 类型的时候,生成一个错误
"alwaysStrict": true, // 以严格模式检查每个模块,并在每个文件里加入 'use strict'
/* 额外的检查 */
"noUnusedLocals": true, // 有未使用的变量时,抛出错误
"noUnusedParameters": true, // 有未使用的参数时,抛出错误
"noImplicitReturns": true, // 并不是所有函数里的代码都有返回值时,抛出错误
"noFallthroughCasesInSwitch": true, // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)
/* 模块解析选项 */
"moduleResolution": "node", // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
"baseUrl": "./", // 用于解析非相对模块名称的基目录
"paths": {}, // 模块名到基于 baseUrl 的路径映射的列表
"rootDirs": [], // 根文件夹列表,其组合内容表示项目运行时的结构内容
"typeRoots": [], // 包含类型声明的文件列表
"types": [], // 需要包含的类型声明文件名列表
"allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。
/* Source Map Options */
"sourceRoot": "./", // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
"mapRoot": "./", // 指定调试器应该找到映射文件而不是生成文件的位置
"inlineSourceMap": true, // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
"inlineSources": true, // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性
/* 其他选项 */
"experimentalDecorators": true, // 启用装饰器
"emitDecoratorMetadata": true // 为装饰器提供元数据的支持
}
}
补充内容
.d.ts
- JS文件 + .d.ts文件 === ts文件
- .d.ts文件可以让JS文件继续维持自己JS文件的身份,而拥有TS的类型保护,一般写业务代码时不会用到,但是点击类型跳转一般会跳转到对应的.d.ts文件,可以当做是JS的类型说明和约束文件
常见问题处理
Object.keys遍历报错的问题
- 错误场景一
const foo = {
a: '1',
b: '2'
}
// 这里会有typescript的错误提示
const getPropertyValue = Object.keys(foo).map(item => foo[item])
// 解决方案
// 这里typeof foo => foo的类型 等同于 interface Foo { a: string; b: string; }
// typeof foo === Foo,这里只所以用 typeof foo,因为这样方便,对于不想写interface的直接量对象很容易获取它的类型
// keyof typeof foo这里只获取 Foo的类型的key值,注意这个keyof后面一定是 typescript的类型
type FooType = keyof typeof foo;
var getPropertyValue = Object.keys(foo).map(item => foo[item as FooType])
- 错误场景二
const foo = {
a: '1',
b: '2'
}
// 这里也会提示obj会有any类型
function getPropertyValue(obj, key) {
return obj[key]
}
// 解决方案
// 这里声明了两个泛型 T 和 K
// T 代表函数第一个参数的类型,K 代表函数第二个参数的类型这个类型指向第一个参数类型中包含的key的值
function getPropertyValue<T, K extends keyof T>(obj:T, key:K):T[K] {
return obj[key]
}
getPropertyValue(foo, 'a')