学习 Typescript 入门(一篇文章学完TS基础)

338 阅读14分钟

1.TypeScript

是JavaScript 的一个超集,本质上是向这个语言添加可选的静态类型和基于类的面向对象编程。

2.TypeScript 基础类型

2.1 Boolean

2.2 Number

2.3 String

2.4 Array

2.5 Enum

枚举类型,使用枚举可以定义一些带名字的常量,使用枚举可以清晰地表达意图。TS 支持数字的和基于字符串的枚举。

2.5.1 数字枚举
enum Zn {
	AA,
	BB,
	CC
}
let zhang: Zn = Zn.AA

默认 AA 初始值为 0,然后默认增长,BB 为 1 ...

也可以设置初始值:后面接着就是自增的

enum Zn {
	AA = 6,
	BB,
	CC
}
let zhang: Zn = Zn.AA
2.5.2 字符串枚举

在一个字符串枚举中,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。

enum Zn {
  AA = "AAAA",
  BB = "BBBB",
  CC = "CCCC"
}

// 相当于 type Zn = 'AAAA' | 'BBBB' | 'CCCC'
2.5.3 异构枚举

异构枚举是数字和字符串的混合

enum Enum {
  AA,
  BB,
  CC = "C",
  D = 8,// CC为字符串,后面必须要设置默认值,否则会报错
  E
}

2.6 any

任何类型都可以被归为 any 类型。any 类型成为了顶级类型(全局超级类型)。

any 类型本质上是类型系统的一个逃逸舱,不进行任何的检查。使用了 any 类型,就无法使用 TS 提供的类型保护,开发中尽量避免使用 any。

搞个栗子吃吃:

let zn: any;

zn.a.b; // OK
zn.trim(); // OK
zn(); // OK
new zn(); // OK
zn[0][1]; // OK

2.7 unknown

所有类型都可以赋值给 any 也可以赋值给 unknow,让 unknown 成为 TS 类型中的另一种顶级类型。

但是 unknow 类型的值只能赋值给 unknow 和 any,不能赋值给别的任意类型。

搞个和 any 中相同的栗子:

let zn: any;

zn.a.b; // Error
zn.trim(); // Error
zn(); // Error
new zn(); // Error
zn[0][1]; // Error

将 value 变量类型设置为 unknow 之后,这些操作都不再被认为是类型正确的。any 类型变为 unknow 类型,将禁止任何修改。(unknown 没有被断言或细化到一个确切类型之前,是不允许在其上进行任何操作的。)

any 可以赋值给任意类型,也可以接收任意类型的赋值
unknown 在没有明确它类型的情况下,不可以使用上面的任何属性和方法
所以,unknown 比 any 更安全

大白话就是:any 可以默认为任何类型,unknow 默认不知道是哪个类型,所以any可以执行任何操作,unknow 不可以执行任何操作(因为不知道是什么类型)

2.8 Tuple

数组一般由同种类型的值组成,但有时候需要在单个变量中存储不同类型的值,这时候就需要使用元组。JS 中是没有元组的,元组是 TS 中特有的类型,类似数组

用于定义具有有限数量的未命名属性的类型。每个属性都有自己的类型。使用元组时,必须提供每个属性的值。

举个栗子瞅瞅:

let zn: [string, number];
zn = ["zhangning", 24];

上面的类型是一个类型数组,如果每一项的类型不匹配,则会报错

2.9 void

可以理解为 void 类型与 any 类型相反,表示没有任何类型。通常用于一个函数没有返回值时,返回值类型是 void

// 声明函数返回值为void
function znUser(): void {
  console.log("aaa");
}

注意:生命一个void类型的变量没有什么作用,因为值只能为 undefined 或 null

2.10 null 和 undefined

在 TS 里面 undefined 和 null 有各自的类型 undefined 和 null

let z: undefined = undefined;
let n: null = null;

默认情况下 null 和 undefined 是所有类型的子类型,就是可以把 null 和 undefined 赋值给 number类型。但是如果启用了strictNullChecks (严格空值检查),null 和 undefined 只能赋值给 void 和它们各自的类型。

2.11 never

表示用不存在的值的类型,never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。

// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
  throw new Error(message);
}

function aa(): never {
  while (true) {}
}

TS 中可以利用 never 类型的特性来实现全面性检查:

type Zn = string | number;

function controlFlowAnalysisWithNever(zn: Zn) {
  if (typeof zn === "string") {
    // 这里 foo 为 string 类型
  } else if (typeof zn === "number") {
    // 这里 foo 为 number 类型
  } else {
    // foo 在这里是 never
    const check: never = foo;
  }
}

3. TypeScript 断言

在你清楚地知道一个实体具有比它现有类型更确切的类型时通常会使用断言。

就是告诉编译器“我知道自己在干什么”。类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和结构。

断言有两种形式:

3.1 尖括号 语法

let zn: any = "zhangning";
let znLength: number = (<string>zn).length;

3.2 as 语法

let zn: any = "zhangning";
let znLength: number = (zn as string).length;

在开发中,定义 setTimeout 和 setInterval 的时候使用下面比较多:

let time:Return<typeof setTimeout> | null;

4. 类型守卫

类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定范围内。换句话说,类型保护可以保证一个字符串是一个字符串,尽管他的值也可以是要给数字。类型保护和特性检测并不是完全不同,其主要思想是尝试检测属性、方法或原型,以确定如何处理值。目前主要有四种的方式来实现类型保护:

4.1 in 关键字

interface Zn {
  name: string;
  play: string[];
}

interface Zn1 {
  name: string;
  age: number;
}

type Aaa = Zn | Zn1;

function printEmployeeInformation(emp: Aaa) {
  console.log("Name: " + emp.name);
  if ("age" in emp) {
    console.log("age: " + emp.age);
  }
  if ("play" in emp) {
    console.log("play: " + emp.play);
  }
}

4.2 typeof 关键字

function f(v: string, p: string | number) {
  if (typeof p === "number") {
      return Array(p + 1).join(" ") + v;
  }
  if (typeof p === "string") {
      return padding + v;
  }
  throw new Error(`Expected string or number, got '${p}'.`);
}

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. 联合类型和类型别名

5.1 联合类型

联合类型通常与 null 或 undefined 一起使用:

const time: ReturnType<typeof setTimeout>|null;

同时接受两种或多种类型成为联合类型

5.2 可辨识联合

也称为代数数据类型或标签联合类型。它包含3个要点:可辨识、联合类型和类型守卫

这种类型的本质是结合联合类型和字面量类型的一种类型保护方法。如果一个类型是多个类型的联合类型,且多个类型含有一个公共属性,那么就可以利用这个公共属性,来创建不同的类型保护区块。

5.2.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
}

上面的几个接口都含有一个 vType 属性,该属性被称为可辨识的属性,而其他的属性只跟特性的接口相关。

5.2.2 联合类型

基于上面定义的接口可以创建一个联合类型:

type Vehicle = Motorcycle | Car | Truck;
5.2.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);

这里的代码会报错,因为在 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;
  }
}

上面使用了 switchcase 运算符来实现类型守卫,从而确保在 evaluatePrice ****方法中,我们可以安全地访问 vehicle 对象中的所包含的属性,来正确计算车辆类型所对应的价格。

5.3 类型别名

类型别名是给一个类型起个新名字

type Message = string | string[];// 这个就是类型别名,哈哈,文字又坑我

let greet = (message: Message) => {
  // ...
};

6. 交叉类型

TS 交叉类型是将多个类型合并为一个类型。这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。

interface IPerson {
  id: string;
  age: number;
}

interface IWorker {
  companyId: string;
}

type IStaff = IPerson & IWorker;// 就是把两个类型合在一起,哈哈,文字又搞我

const staff: IStaff = {
  id: 'E1006',
  age: 33,
  companyId: 'EFT'
};

console.dir(staff)

通过 & 运算符定义了 IStaff 交叉类型,包含两个类型的所有类型成员

7. TypeScript 函数

7.1 TypeScript 函数与 JavaScript 函数的区别

TypeScriptJavaScript
含有类型无类型
箭头函数箭头函数(ES2015)
函数类型无函数类型
必填和可选参数所有参数都是可选的
默认参数默认参数
剩余参数剩余参数
函数重载无函数重载

7.2 参数类型和返回类型

function createUserId(name: string, id: number): string {
  return name + id;
}

7.3 函数类型

let IdGenerator: (chars: string, nums: number) => string;

function createUserId(name: string, id: number): string {
  return name + id;
}

IdGenerator = createUserId;

7.4 可选参数及默认参数

// 可选参数
function createUserId(name: string, age?: number, id: number): string {
  return name + id;
}

// 默认参数
function createUserId(
  name: string = "Semlinker",
  age?: number,
  id: 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 函数重载

使用相同的名字和不同的参数数量或类型创建多个方法的一种能力。就是为同意函数提供多个函数类型定义来进行函数重载,编译器会根据这个列表去处理函数的调用。

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) {
  if (typeof a === "string" || typeof b === "string") {
    return a.toString() + b.toString();
  }
  return a + b;
}

上面,为 add 函数提供了多个函数类型定义,从而实现函数的重载。在TS中除了可以重载普通函数之外,还可以重载类中的成员方法。

方法重载是指在同一个类中方法同名,参数不同(参数类型、个数或个数相同的同时顺序不同),调用时根据实参的形式,自动匹配方法执行操作的一种技术。类中成员方法满足重载的条件是:在同一个类中,方法名相同参数列表不同。

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");

当TS编译器处理函数重载时,他会查找重载列表,尝试使用第一个重载定义。如果匹配就使用这个。在定义时一定要把最精确的定义放在最前面。在 Calculator 类中,add(a:Combinable,b:combinable){} 并不是重载列表的一部分,因此对于 add 成员方法来说,我们只定义了四个重载方法。

8. TypeScript 数组

8.1 数组解构

let x: number; let y: number ;let z: number;
let five_array = [0,1,2,3,4];
[x,y,z] = five_array;

8.2数组扩展运算符

let two_array = [0, 1];
let five_array = [...two_array, 2, 3, 4];

8.3数组遍历

let colors: string[] = ["red", "green", "blue"];
for (let i of colors) {
  console.log(i);
}

9. TypeScript 对象

9.1对象解构

let person = {
  name: "Semlinker",
  gender: "Male",
};

let { name, gender } = person;

9.2 对象展开运算符

let person = {
  name: "Semlinker",
  gender: "Male",
  address: "Xiamen",
};

// 组装对象
let personWithAge = { ...person, age: 33 };

// 获取除了某些项外的其它项
let { name, ...rest } = person;

10. TypeScript 接口

接口是对行为的抽象,具体如何行动需要由类去实现

TS 的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对 【对象的形状】 进行描述。

10.1 对象的形状

interface Person {
  name: string;
  age: number;
}

let Semlinker: Person = {
  name: "Semlinker",
  age: 33,
};

10.2 可选 | 只读属性

interface Person {
  readonly name: string;
  age?: number;
}

只读属性用于限制只能在对象刚刚创建的时候修改其值。此外 TS 还提供了ReadonlyArray 类型,它与 Array 相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改。

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!

11. TypeScript 类

11.1 类的属性与方法

类是一种面向对象计算机编程语言的构造,是创建对象的蓝图,描述了所创建的对象共同的属性和方法。

在 TS 中,可以通过 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 访问器

在 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);
}

11.3 类的继承

继承(Inheritance)是一种连接类与类的层次模型。指的是一个类继承另外一个类的功能,并可以增加自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系。

TS 中使用 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.4 ECMAScript 私有字段

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;
// 属性“#name”在类“Person”外不可访问
// 因为它有一个私有标识符 #

与常规属性不同,私有字段要牢记以下规则:

私有字段以 # 字符开头,有时我们称之为私有名称;

每个私有字段名称都为一地限定于其包含的类;

不能在私有字段上使用 TypeScript 可访问性修饰符(public 或 private)

私有字段不能在包含的类之外访问,甚至不能被检测到。

12. TypeScript 泛型

泛型的目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员,类的方法,函数参数和函数返回值。

泛型(Generics)是允许同一个函数接受不同类型参数的一种模板。相比于使用 any 类型,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类型。

12.1 泛型接口

interface GenericIdentityFn<T> {
  (arg: T): T;
}

12.2 泛型类

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.3 泛型变量

下面是经常碰到的一些约定式的规范,看一下常见的泛型变量代表的意思(当然随意写字符也可以,这里只能说大家约定的一种规范而已):

T(Type):表示一个 TypeScript 类型

K(Key):表示对象中的键类型

V(Value):表示对象中的值类型

E(Element):表示元素类型

12.4 泛型工具类型

TS 内置了一些常用的工具类型,比如 Partial、Required、Readonly、Record 和 ReturnType 等。

先了解一下其他的工具类型

1. typeof

在 TS 中,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[]
2. 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
3. in

in 用来遍历枚举类型

type Keys = "a" | "b" | "c"

type Obj =  {
  [p in Keys]: any
} // -> { a: any, b: any, c: any }
4. infer

在条件类型与剧中,可以用 infer 生命一个类型变量并且对它进行使用

type ReturnType<T> = T extends (
  ...args: any[]
) => infer R ? R : any;

infer R 就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用。

----------------- 这里详细学一下 infer --------------------

你知道怎么获取T0数组类型中元素的类型和T1函数类型中的返回值类型么?

type T0 = string[];
type T1 = () => string;

要实现上面的功能,需要使用类型模式匹配技术 -- 条件类型 + infer。条件类型允许我们检测两种类型之间的关系,通过条件类型我们可以判断两种类型是否相兼容。而 infer 用于声明类型变量,以存储在模式匹配过程中所捕获的类型。

看下如何捕获 T0 数组类型中元素的类型:

type UnpackedArray<T> = T extends (infer R) ? R : T
type U0 = UnpackedArray<T0> // string

上面代码 T extends (infer R) ? R : T 是条件类型的语法,而 extends 子句中的 infer R 引入了一个新的类型变量 R,用于存储被推断的类型

演示下执行流程:

type U0 = UnpackedArray<T0>
 
// T => T0: string[]
type UnpackedArray<string[]> = string[] extends (infer R)[] ? R : string[] 
// string[] extends (infer R)[] 模式匹配成功
// R => string

注意:infer 只能在条件类型的 extends 子句中使用,同时 infer 声明的类型变量只在条件类型的 true 分支中可用。

type Wrong1<T extends (infer R)[]> = T[0] // Error
type Wrong2<T> = (infer R)[] extends T ? R : T // Error
type Wrong3<T> = T extends (infer R)[] ? T : R // Error

下面看下如何获取 T1 函数类型的返回值类型:

type UnpackedFn<T> = T extends (...args: any[])=> infer R ? R : T

当遇到函数重载的时候,TS 将使用最后一个调用签名进行类型推断:

declare function foo(x: string): number;
declare function foo(x: number): string;
declare function foo(x: string | number): string | number;
 
type UnpackedFn<T> = T extends (...args: any[]) => infer R ? R : T;
type U2 = UnpackedFn<typeof foo>;  // string | number

再搞个栗子

type Unpacked<T> =
    T extends (infer R)[] ? R :
    T extends (...args: any[]) => infer R ? R :
    T extends Promise<infer R> ? R :
    T;
 
type T0 = Unpacked<string>;  // string
type T1 = Unpacked<string[]>;  // string
type T2 = Unpacked<() => string>;  // string
type T3 = Unpacked<Promise<string>>;  // string
type T4 = Unpacked<Promise<string>[]>;  // Promise<string>
type T5 = Unpacked<Unpacked<Promise<string>[]>>;  // string

上面代码中,Unpacked 工具类型利用了条件类型和条件链,轻松实现了推断出数组类型中元素的类型、函数类型返回值的类型和 Promise 类型中返回值的类型的功能。

利用条件类型和 infer,还可以推断出对象类型中的健的类型,搞个栗子吃吃

type User = {
  id: number;
  name: string;
}
type PropertyType<T> = T extends {id: infer U, name: infer R} ? [U, R]: T
type U3 = PropertyType<User> // [numbner, string]

在PropertyType 工具类型中,我们通过 infer 声明了两个类型变量 U 和 R,分别表示对象类型中 id 和 name 属性的类型。若类型匹配,会以元组的形式返回 id 和 name 属性的类型。

上面的示例,如果只声明一个类型变量 R,结果是什么?

type PropertyType<T> =  T extends { id: infer R, name: infer R } ? R : T
 
type U4 = PropertyType<User> // string | number

以上可知,U4 类型返回的是 string 和 number 类型组合成的联合类型。为什么会返回这样的结果呢?这是因为在协变位置上,若同一个类型变量存在多个候选者,则最终的类型被推断为联合类型。

然而,在逆变位置上,若同一个类型变量存在多个候选者,则最后的类型将被推断为交叉类型。

type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;
 
type U5 = Bar<{ a: (x: string) => void, b: (x: number) => void }>;  // string & number

?????逆变位置是什么意思,没搞懂

这里再学习下 逆变和协变,哈哈,知识点就这样丰富了起来,撸起袖子加油干

 
declare let x: number
declare let y: 1
x = y; // OK
y = x; // Error: Type 'number' is not assignable to type '1'.
 
declare let fx : (a: number) => void;
declare let fy : (a: 1) => void;
 
fx = fy; // Error: Type 'number' is not assignable to type '1'.
fy = fx; // OK
 
declare let gx : () => number;
declare let gy : () => 1;
 
gx = gy; // OK
gy = gx; // Error: Type 'number' is not assignable to type '1'.
 
1number 的子类型,
() => 1 是 () => number 的子类型
(a: 1) => void 是 (a: number) => void 的父类型
我们可以发现, 参数的子类型关系倒过来了,但是返回值位置的子类型关系依旧保留。
所以函数中,参数是逆变的,返回值是协变的。

可懂?????

上面 U5 类型返回的是 string 和 number 类型组合成的交叉类型,即最终的类型 never 类型

上面已经学完了条件类型和 infer 类型的作用了,再搞个案例来巩固下知识点:

type UnionToIntersection<U> = (
  U extends any ? (arg: U) => void : never
) extends (arg: infer R) => void
  ? R
  : never
5. extends

有时候我们定义的泛型不想过于灵活或者说继承某些类。可以通过 extends 关键字添加泛型约束

interface ILengthwise {
  length: number;
}

function loggingIdentity<T extends ILengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

现在这个泛型函数被定义了约束,因此不再是适用于任意类型

loggingIdentity(3);  
// Error, number doesn't have a .length property

这时需要传入符合约束类型的值,必须包含必须的属性

loggingIdentity({length: 10, value: 3});
6. Partial

Partial 的作用就是将某个类型里的属性全部变为可选项?

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: "organize desk",
  description: "clear clutter",
};

const todo2 = updateTodo(todo1, {
  description: "throw out trash",
});


// 上面 fieldsToUpdate 的类型为 Partial<Todo>
{
   title?: string | undefined;
   description?: string | undefined;
}

13. TypeScript 装饰器

13.1 装饰器是什么

  • 它是一个表达式
  • 表达式被执行后返回一个函数
  • 函数的入参分别为 target、name 和 descriptor
  • 执行函数后,可能返回 descriptor 对象,用于配置 target 对象

13.2 装饰器的分类

  • 类装饰器(Class decorators)
  • 属性装饰器(Property decorators)
  • 方法装饰器(Method decorators)
  • 参数装饰器(Parameter decorators)

13.3 类装饰器

声明:

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 zhangning!");
  };
}

@Greeter
class Greeting {
  constructor() {
    // 内部实现
  }
}

let myGreeting = new Greeting();
myGreeting.greet(); // console output: 'Hello zhangning!';

上面定义了 Greeter 类装饰器,同时使用 @Greeter 语法糖来使用装饰器。

上面输出了 Hello zhangning! ,当然也可以自定义输出

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(); // console output: 'Hello TS!';

13.4 属性装饰器

声明:

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
  });

  // getter
  const getter = function (this: any) {
    const currVal = this[backingField];
    console.log(`Get: ${key} => ${currVal}`);
    return currVal;
  };

  // setter
  const setter = function (this: any, newVal: any) {
    console.log(`Set: ${key} => ${newVal}`);
    this[backingField] = newVal;
  };

  // 对属性操作进行拦截
  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

13.5.方法装饰器

声明:

declare type MethodDecorator = <T>(target:Object, propertyKey: string | symbol,    
  descriptor: TypePropertyDescript<T>) => TypedPropertyDescriptor<T> | void;

用来装饰类的方法。接受三个参数:

target: Object - 被装饰的类

propertyKey: string | symbol - 方法名

descriptor: TRypePropertyDescript - 属性描述符

栗子有点多,那也要吃完:

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 ouput: [{method: "double", output: 22, ...}]
console.log(calc.loggedOutput); 

13.6 参数装饰器

声明:

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; 
  }
}

// console output: The parameter in position 0 
// at Greeter has been decorated

到此入门知识已经学习完了,是不是越发想要放弃对 TS 的追求,哈哈。

学习资源: 全栈修仙之路的文档