TypeScript 学习总结 (三)
前言:TypeScript 内容,会分 3 篇文章来写,可以持续关注哟~
本章主要内容
- 1,ts 类
- 2,ts 泛型
- 3,ts 装饰器
1,ts 类
1.1,类的属性与方法
在面向对象中,类是一种面向对象计算机编程语言的构造,在 ts 中,我们可以通过 class 来定义一个类
class Person {
// 静态属性
static name: string = "xiaowang" // es6的静态方法前如果加 static 关键字,表示该方法不会被实例继承,而是直接通过类来调用
// 成员属性
greeting: string
// 构造函数 - 执行初始化操作
constructor(message: string) {
this.greeting = message
}
// 静态方法
static getClassName() {
return "my name is xiaowang"
}
// 成员方法
greet() {
return "Hello, " + this.greeting
}
}
let p1 = new Person("world")
以上代码编译成 ES5为:
var Person = /** @class */ (function () {
// 构造函数 - 执行初始化操作
function Person(message) {
this.greeting = message
}
// 静态方法
Person.getClassName = function () {
return "my name is xiaowang"
}
// 成员方法
Person.prototype.greet = function () {
return "Hello, " + this.greeting
};
// 静态属性
Person.name = "xiaowang"
return Person
}())
var p1 = new Person("world")
1.2, ECMAScript 私有字段
class Person {
#name: string // 私有变量以 # 开头,我们成为私有名称
constructor(name: string) {
this.#name = name
}
greet() {
console.log(`Hello, my name is ${this.#name}`)
}
}
let p1 = new Person("xiaowang")
不能在私有字段上使用 TypeScript 可访问性修饰符(如 public 或 private),私有字段不能在包含的类之外访问,甚至不能被检测到。
1.3,访问器
在 ts 中,我们可以通过 getter 和 setter 方法来实现数据的封装和有效性校验,防止出现异常数据
let str = "string"
class Person {
private name: string
get fullName(): string {
return this.name
}
set fullName(newName: string) {
if (str && str == "string") {
this.name = newName
} else {
console.log(1)
}
}
}
let p1 = new Person()
p1.fullName = "xiaowang"
console.log(p1)
console.log(p1.fullName)
1.4,类的继承
继承是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,可以增加它自己的新功能的能力, ts 中可以通过 extends
关键字实现继承
class Parent {
name: string
constructor(name: string) {
this.name = 'parent'
this.age = 30
}
getName() {
return `my name is ${this.name}`
}
}
class Son extends Parent {
constructor() {
super()
}
getAge() {
return `age: ${this.age}`
}
}
let s = new Son()
console.log(s.getName())
console.log(s.getAge())
1.5,抽象类
抽象类: 是使用 abstract
关键字声明的类,抽象类不能被实例化,因为它里面包含一个或多个抽象方法。所谓的抽象方法,是指不包含具体实现的方法
abstract class Person {
constructor(public name: string){}
abstract getName() {}
}
const p1 = new Person(); // error TS2511: Cannot create an instance of an abstract class.
抽象类不能被直接实例化,我们只能实例化实现了所有抽象方法的子类:
abstract class Person {
constructor(public name: string){}
// 抽象方法
abstract say(words: string) :void;
}
class Son extends Person {
constructor(name: string) {
super(name);
}
say(words: string): void {
console.log(`${this.name} say ${words}`);
}
}
const s = new Son("xiaowang")
s.say("Hello") // xiaowang say Hello
1.6,类方法重载
重载:一个类中可以有多个方法,方法名相同,方法的参数和类型不同
class Person {
getName(): void
getName(name: string): void
getName(name?: string) {
if(name === 'xiaowang') {
console.log(`我是 ${name}`)
} else {
console.log(`我们不是 ${name}`)
}
}
}
const p1 = new Person()
p1.getName('xiaowang')
p1.getName()
1,ts 泛型
泛型(Generics)是允许同一个函数接受不同类型参数的一种模板。相比于使用 any 类型,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类型
设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值
2.1,泛型语法
<T>
像传递参数一样,可以传递了我们想要用于特定函数调用的类型
function getData <T> (value: T): T {
return value
}
// <T> 是传递类型, 这里的 T 称为 类型变量,它是我们传递给 getData 函数的类型占位符,同时它被分配给 value 参数用来代替它的类型:此时 T 充当的是类型,而不是特定的 Number 类型
// value: T 是链式传递给参数类型
// :T 是返回类型
以上代码,我们使用 getData <Number>(1)
调用时,Number类型就像参数 1 一样,它将在出现 T 的任何位置填充该类型。
其中 T 代表 Type,在定义泛型时通常用作第一个类型变量名称。但实际上 T 可以用任何有效名称代替。除了 T 之外,以下是常见泛型变量代表的意思
- K(Key):表示对象中的键类型
- V(Value):表示对象中的值类型
- E(Element):表示元素类型
其实并不是只能定义一个类型变量,我们可以引入希望定义的任何数量的类型变量。比如我们引入一个新的类型变量 U,用于扩展我们定义的 getData 函数:
function getData <T, U> (value: T, message: U): T {
console.log(message)
return value
}
console.log(getData<Number, string>(100, 'xiaowang'))
除了为类型变量显式设定值之外,一种更常见的做法是使编译器自动选择这些类型,从而使代码更简洁。我们可以完全省略尖括号,比如:
function getData <T, U> (value: T, message: U): T {
console.log(message)
return value
}
console.log(getData(100, 'xiaowang'))
以上代码,编译器足够聪明,能够知道我们的参数类型,并将它们赋值给 T 和 U,而不需要我们显式指定它们
2.2,泛型接口
interface getData<T> {
(arg: T): T
}
2.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
}
2.4, 泛型工具类型
ts 常见的工具类型,如 Partial、Required、Readonly、Record 和 ReturnType 等
2.4.1, typeof
在 ts 中,typeof 操作符可以用来获取一个变量声明或对象的类型
interface Person {
name: string
age: number
}
const p1: Person = { name: 'xiaowang', age: 18 }
type person1 = typeof p1 // object -> Person
function toArray(x: number): Array<number> {
return [x]
}
type Func = typeof toArray // function
2.4.2, keyof
keyof 操作符用于获取某种类型的所有键,其返回类型是联合类型
interface Person {
name: string;
age: number;
}
type p1 = keyof Person; // "name" | "age"
type p2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join"
type p3 = keyof { [x: string]: Person }; // string | number
在 ts 中支持两种索引签名,数字索引和字符串索引:
interface StringArray {
// 字符串索引 -> keyof StringArray => string | number
[index: string]: string;
}
interface StringArray1 {
// 数字索引 -> keyof StringArray1 => number
[index: number]: string;
}
为了同时支持两种索引类型,就得要求数字索引的返回值必须是字符串索引返回值的子类。其中的原因就是当使用数值索引时,Js 在执行索引操作时,会先把数值索引先转换为字符串索引。所以 keyof { [x: string]: Person } 的结果会返回 string | number
2.4.3, in
用来遍历枚举类型:
type str = "a" | "b" | "c"
type Obj = {
[p in str]: any
} // -> { a: any, b: any, c: any }
2.4.4, infer
在条件类型语句中,可以用 infer 声明一个类型变量并且对它进行使用
type ReturnType<T> = T extends (
...args: any[]
) => infer R ? R : any
以上代码中 infer R 就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用
2.4.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});
2.4.6, Partial
Partial<T>
的作用就是将某个类型里的属性全部变为可选项 ?
定义:
type Partial<T> = {
[P in keyof T]?: T[P]
}
在以上代码中,首先通过 keyof T
拿到 T
的所有属性名,然后使用 in
进行遍历,将值赋给 P
,最后通过 T[P]
取得相应的属性值。中间的 ?
号,用于将所有属性变为可选
interface Todo {
title: string;
description: string;
}
function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
return { ...todo, ...fieldsToUpdate };
}
const todo1 = {
title: "Learn TS",
description: "Learn TypeScript",
};
const todo2 = updateTodo(todo1, {
description: "Learn TypeScript Enum",
})
在上面的 updateTodo
方法中,我们利用 Partial<T>
工具类型,定义 fieldsToUpdate
的类型为 Partial<Todo>
,即:
{
title?: string | undefined;
description?: string | undefined;
}
3, 装饰器
装饰器是一个表达式,该表达式被执行后,返回一个函数,函数的入参分贝是target、name和descriptor,执行函数后,可能返回 descriptor 对象,用于配置 target 对象
装饰器 种类:
- 类装饰器
- 属性装饰器
- 方法装饰器
- 参数装饰器
需要注意的是,若要启用实验性的装饰器特性,你必须在命令行或 tsconfig.json 里启用 experimentalDecorators 编译器选项:
// 命令
tsc --target ES5 --experimentalDecorators
// tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
3.1,类装饰器
类装饰器声明:
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 xiaowang")
}
}
@Greeter
class Greeting {
constructor() {
}
}
let greet = new Greeting()
(greet as any).greet() // console output: 'Hello xiaowang'
以上代码定义了 Greeter 类装饰器,同时我们使用了 @Greeter 语法糖,来使用装饰器
function Greeter(greeting: string) {
return function (target: Function) {
target.prototype.greet = function (): void {
console.log(greeting)
}
}
}
@Greeter("Hello Jerry")
class Greeting {
constructor() {
}
}
let greet = new Greeting()
(greet as any).greet(); // console output: 'Hello Jerry'
3.2,属性装饰器
属性装饰器声明:
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("xiaowang")
p1.name = "Jerry"
以上代码我们定义了一个 logProperty 函数,来跟踪用户对属性的操作,当代码成功运行后,在控制台会输出以下结果
Set: name => xiaowang
Set: name => Jerry
3.3,方法装饰器
方法装饰器声明:
declare type MethodDecorator = <T>(target:Object, propertyKey: string | symbol,
descriptor: TypePropertyDescript<T>) => TypedPropertyDescriptor<T> | void
// 方法装饰器顾名思义,用来装饰类的方法。它接收三个参数:
// target: Object - 被装饰的类
// propertyKey: string | symbol - 方法名
// descriptor: TypePropertyDescript - 属性描述符
function log(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
let originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log("wrapped function: before invoking " + propertyKey);
let result = originalMethod.apply(this, args);
console.log("wrapped function: after invoking " + propertyKey);
return result;
};
}
class Task {
@log
runTask(arg: any): any {
console.log("runTask invoked, args: " + arg);
return "finished";
}
}
let task = new Task();
let result = task.runTask("learn ts");
console.log("result: " + result)
以上代码成功运行后,控制台会输出以下结果:
"wrapped function: before invoking runTask"
"runTask invoked, args: learn ts"
"wrapped function: after invoking runTask"
"result: finished"
3.4,参数装饰器
参数装饰器声明:
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol,
parameterIndex: number ) => void
// 参数装饰器顾名思义,是用来装饰函数参数,它接收三个参数:
// 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"