TypeScript 学习总结 (三)

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)
image
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())
image
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()
image

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'))
image

除了为类型变量显式设定值之外,一种更常见的做法是使编译器自动选择这些类型,从而使代码更简洁。我们可以完全省略尖括号,比如:

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" 
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,332评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,930评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,204评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,348评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,356评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,447评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,862评论 3 394
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,516评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,710评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,518评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,582评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,295评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,848评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,881评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,121评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,737评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,280评论 2 341

推荐阅读更多精彩内容