前端迅速通关TS

看完通关TS,不做标题党

0.准备

安装环境

# 全局安装TS
npm i typescript -g

# 检查TS版本
tsc -v

# 安装tsc-node
npm i tsc-node -g

# 生产TS配置文件
tsc --init

1.基础类型

  1. 字符串类型

    let a: string = 'asdf'
  2. 数字类型

    JS中任意数字类型都没问题。

    let notANumber: number = NaN;//Nan
    let num: number = 123;//普通数字
    let infinityNumber: number = Infinity;//无穷大
    let decimal: number = 6;//十进制
    let hex: number = 0xf00d;//十六进制
    let binary: number = 0b1010;//二进制
    let octal: number = 0o744;//八进制s
  3. 布尔类型

    let flag: boolean = true

    注意一点的是使用new Boolean()会报错,因为new Boolean()结果是一个对象。

    // 不能将类型“Boolean”分配给类型“boolean”。
    // “boolean”是基元,但“Boolean”是包装器对象。如可能首选使用“boolean”。
    let flag: boolean = new Boolean(1)
  4. 空值类型

    用来表示一个函数没有返回值。

    function fn(): void {
        console.log('log')
    }
  5. nullundefined类型

    undefined表示声明了没有赋值,null表示声明以后赋值了空值。

    let u: undefined = undefined;//定义undefined
    let n: null = null;//定义null

    可以把undefined赋值给void

    let u: void = undefined

    非严格模式下,也可以把null赋值给void,但是严格模式下不行:

    // 严格模式下会出现报错提示:
    // 不能将类型“null”分配给类型“void”。
    let n: void = null;

    非严格模式下nullundefined可以赋值给任何类型,但是在严格模式下null只能赋值给null或者any类型;undefined只能赋值undefinedvoid,或者any类型。

    let n1: any = null
    let n2: null = null
    let n3: undefined = null // 报错
    
    let u1: any = undefined;
    let u2: null = undefined; // 报错
    let u3: undefined = undefined;

2.any和unknow

  1. 所有的类型都可以赋值给any类型,any类型也可以,是完全跳过了TS的检查。

    let anyValue: any = 10;
    let str1: string = anyValue; 
  2. 所有类型也都可以赋值给unkonw类型,但是unknown 类型的值只能赋值给 unknownany 类型。

    let unknownValue: unknown = "hello";
    let str2: string = unknownValue; // 错误:不能将类型 'unknown' 分配给类型 'string'
  3. any 可以直接操作,不进行类型检查。

    console.log(anyValue.length); // 允许(即使可能运行时出错)
    anyValue.callSomeMethod(); // 允许(即使可能运行时出错)
  4. unknown 不能直接操作。

    console.log(unknownValue.length); // 错误:对象的类型为 'unknown'
    unknownValue.callSomeMethod(); // 错误:对象的类型为 'unknown'

3.接口和对象类型

在TS中,我们用来约束对象的类型主要用的就是interface(接口)。

//这样写是会报错的 因为我们在person定义了a,b但是对象里面缺少b属性
//使用接口约束的时候不能多一个属性也不能少一个属性
//必须与接口保持一致
interface Person {
    b: string,
    a: string
}

// 报错提示少了b属性
const person: Person = {
    a: "213"
}

重名的interface会进行合并:

interface Person {
    a: string,
}
interface Person {
    b: number,
}

// 最终Person接口定义的类型是:
// interface Person {
//     a: string
//     b: number,
// }

也可以可以使用extends继承:

interface A {
    a: string,
}

interface B extends A {
    b: number
}

// 最终B的定义的接口是
// interface B {
//     a: string,
//     b: number
// }

可以使用?来定义这个属性为可选属性,意思就是有没有都行:

interface A {
    a: string,
    b?: number
}

// 并没有报错提示说少了b属性,因为b是可选的
let a: A = {
    a: '123'
}

有一种情况就是我们不知道会对这个对象添加什么属性,这个时候可以使用任意属性:

// 我们定义了[propName: string]: any;
// 允许添加新的任意属性
interface Test {
    a?: string,
    b: string,
    [propName: string]: any;
}

// 我们添加了c属性和d属性都没有出现报错
const obj: Test = {
    a: "213",
    b: "123",
    c: 'asdfasdf',
    d: true
}

// 需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集
// 我们定义了[propName: string]: boolean;
interface Test {
    a?: string,
    b: string,
    [propName: string]: boolean;
}

// 这个时候c属性会报错,因为string并不是boolean的子集
const obj: Test = {
    a: "213",
    b: "123",
    c: 'asdfasdf',
    d: true
}

当我们不希望有一些属性会被更改,就在属性前面加上readonly来表示这个属性可以获取,但是不能改:

interface Test {
    readonly a: string,
}

const obj: Test = {
    a: "213",
}

// 会有报错提示:无法为“a”赋值,因为它是只读属性。
obj.a = 123

为这个对象添加一个方法可以这样写:

// 用到了前面说过的void,表示这个函数没有返回值
interface Person {
    a?: string,
    toEat: () => void
}

const person: Person = {
    a: "213",
    toEat: () => {
        console.log(123)
    }
}

4.数组类型

用来限制数组的类型

  1. 类型

    // 自由发挥,例如arr1就表示数字类型的数组
    let arr1: number[] = [123]
    
    // 字符串类型数组
    let arr2: string[] = ['1', '2', '3', '1']
    
    // any类型的数组
    let arr3: any[] = [1, 2, 3,]
    
    // 定义好类型以后,不能放入其他的类型
    let arr4: number[] = [1, 2, 3, '4'] // 会报错,因为有一个字符串
    arr4.push('5')// 也会报错,不能通过方法加入不符合类型的元素
  2. 数组泛型

    关于泛型,后面还会再提到。

    let arr1: Array<number> = [1, 2, 3, 4, 5]
    let arr2: Array<string> = ['1', '2', '3', '4', '5']
    let arr3: Array<boolean> = [true, false, true]
  3. 用接口表示数组

    interface NumberArray {
        [index: number]: number;
    }
    let arr: NumberArray = [1, 1, 2, 3, 5];
    //表示:只要索引的类型是数字时,那么值的类型必须是数字。
  4. arguments类数组

    function Arr(...args: any[]): void {
        // args是一个数组,打印可以看到结果
        let arr1: any[] = args
        console.log(arr1)
    
        // 但是arguments不是一个数组,它是一个类数组对象,这样写会报错的
        let arr2: any[] = arguments
        console.log(arr2);
    
        // 我们应该使用IArguments,这个是TS内置的可以直接使用
        let arr3: IArguments = arguments
        console.log(arr3);
    }
    Arr(111, 222, 333)

5.函数类型

// 和定义对象的接口有点像,不能多传,也不能少传
const fn = (name: string, age:number): string => {
    return name + age
}
fn('张三',18)

// 也可以通过?来表示这个参数是可选的
const fn2 = (name: string, age?: number): string => {
    return name + age
}
fn2('张三', 18)

// 还可以给定默认值,给定了默认值不传也不会报错
const fn3 = (name: string = 'zs'): string => {
    return name
}
fn3()

还可以使用interface来定义函数:

//定义参数 num 和 num2  :后面定义返回值的类型
interface Add {
    (num: number, num2: number): number
}

const fn: Add = (num: number, num2: number): number => {
    return num + num2
}
fn(5, 5)


interface User {
    name: string;
    age: number;
}
function getUserInfo(user: User): User {
    return user
}

有的时候不知道后面会传入几个参数,可以使用...items:any[]

const fn = (array:number[],...items:any[]):any[] => {
       console.log(array,items)
       return items
}
 
let a:number[] = [1,2,3]
fn(a,'4','5','6')

函数重载:先声明多个函数签名(不包含实现),然后提供一个实现签名(通常使用更宽泛的类型)来处理所有情况。

// 这里看不懂没有关系,后面会说typeof的使用方法
// 这段代码大概得意思就是说,a和b可能是string,也有可能是number
// 当a: number, b: number的时候返回值就是number
// 当a: string, b: string的时候返回值就是string
// 当a: string, b: number或者a: number, b: string会直接报错提示,因为没有这样的签名

// 函数重载签名
function add(a: number, b: number): number
function add(a: string, b: string): string

// 实现签名(必须兼容所有重载签名)
function add(a: number | string, b: number | string) {
    if (typeof a === "number" && typeof b === "number") {
        return a + b
    }
    if (typeof a === "string" && typeof b === "string") {
        return a + b
    }
}

// 使用:
const num = add(1, 2);       // 返回类型是 number
const str = add("Hello", "World");  // 返回类型是 string

6.联合类型 | 交叉类型

联合类型就像是“或”的关系:

// 这就表示了a的类型是string或者number,反正两种都行
let a: string | number = '123'
a = 123

同理,交叉类型像是“且”的关系,寻找两种类型之间的交叉:

// 要注意,必须要有公共交叉部分,不然就是unknow类型
let a: string & number // a的类型为unknow

interface A {
    a: string
}

interface B {
    b: number
}
// obj的类型为:
// interface Obj {
//      a: string,
//        b: number
// }
let obj: A & B = {
    a: '123123',
    b: 123123
}

7.类型断言

类型断言就是告诉TS,我确定这个是什么类型,你不要想太多:

interface A {
       run: string
}
 
interface B {
       build: string
}
 
const fn = (type: A | B): string => {
// 这样写是有警告的,因为type是A或者B类型,但是B类型上没有run方法
       return type.run
}

const fn2 = (type: A | B): string => {
// 我们可以使用类型断言告诉TS,我确定传进来的一定是A类型,你不要管我
       return (type as A).run
}

假如说有一个数组,数组里面的元素可以随意改变,我们又不想让他改变可以怎么做呢?

// 使用 as const
let arr = [112, 23, 5456,] as const

// 报错提示 无法为“0”赋值,因为它是只读属性。
arr[0] = 789

8.Class类

TS可以对类进行约束:

// 先写一个VueCls准备试下实现Vue类
interface Options {
    el: string | HTMLElement
}

interface VueCls {
    options: Options
    init(): void
}

// 然后写上以下代码,应该会有提示
// implements:实现,实施
class Vue implements VueCls {

}

鼠标一点就能自动的实现了,但是还是会有报错的提示的,因为类中options没有赋值,我们需要手动写constructor来完成赋值:

interface Options {
    el: string | HTMLElement
}

interface VueCls {
    options: Options
    init(): void
}


class Vue implements VueCls {
    options: Options
    // 或者通过!操作符告诉TS这个值一定会有,你不要担心,这样不赋值也不会报错
    // options!: Options
    constructor(options: Options) {
        this.options = options
    }

    init(): void {
        throw new Error("Method not implemented.")
    }
}

类的3个修饰符:

  1. public

    默认就是public

    // 其实默认就是public
    class Person {
        public name: string;
        public constructor(name: string) {
            this.name = name;
        }
    }
    const person = new Person("Alice");
    console.log(person.name); // 可以访问
  2. private

    成员仅限当前类内部访问,子类和外部均不可访问。

    class Person {
        private age: number;
        constructor(age: number) {
            this.age = age;
        }
        getAge() {
            return this.age; // 类内部可以访问
        }
    }
    const person = new Person(30);
    // console.log(person.age); // 错误:属性"age"为私有属性,只能在类"Person"中访问
  3. protected

    成员允许在当前类和子类中访问,外部不可访问。

    class Person {
        protected age: number;
        constructor(age: number) {
            this.age = age;
        }
    }
    
    class Employee extends Person {
        getAge() {
            return this.age; // 子类中可以访问
        }
    }
    
    const employee = new Employee(25);
    // console.log(employee.age); // 错误:属性"age"受保护,只能在类"Person"及其子类中访问
  4. readonly

    属性只能在声明时或构造函数中初始化,之后不可修改。

    class Person {
        readonly name: string;
        constructor(name: string) {
            this.name = name;
        }
        changeName() {
            // this.name = "Bob"; // 错误:无法分配到"name",因为它是只读属性
        }
    }
  5. static

    class MathHelper {
        static PI: number = 3.14159;
        static calculateCircumference(radius: number): number {
            return 2 * MathHelper.PI * radius;
        }
    }
    console.log(MathHelper.PI); // 通过类名访问静态属性
  6. abstract

    // 要注意的是abstract抽象方法只能出现在abstract抽象类中
    // 意思就是方法前面加了abstract修饰符,那么类前面也要加abstract
    abstract class Animal {
        abstract makeSound(): void; // 抽象方法,没有实现
        
        move(): void {
            console.log("Moving...");
        }
    }
    
    class Dog extends Animal {
        makeSound() {
            console.log("Woof!");
        }
    }
    
    // const animal = new Animal(); // 错误:无法创建抽象类的实例
    const dog = new Dog();
    dog.makeSound(); // "Woof!"

9.元组

个人理解其实也是一个数组,不过在定义的时候限制了数组的元素的个数以及类型。

// 这是一个普通的数组,arr的类型为(string | number)[]
let arr = [1223, '123213']

// arr2就可以说是一个元组,限制了数组的类型和个数[string, number, boolean]
let arr2: [string, number, boolean] = ['123123', 123123, true]

// 元组可以越界,但是越界也会受到类型的限制
arr2.push(false) // 可以
arr.push({ a: 123 }) // 会有报错提示:类型“{ a: number; }”的参数不能赋给类型“string | number”的参数。

// 还可以给每个类型加上tag,看上去会更清晰,比如:
let arr3: [name: string, age: number] = ['zs', 18]
// 可以获取对应tag的类型,比如:
type A = typeof arr3[0] // 获取到类型为string
type B = typeof arr3[1] // 获取到类型为number
// 可以决定那个元素是可选的
let arr4: [name: string, age?: number] = ['zs'] // 不写第二个元素不会报错

10.枚举

// number类型的枚举
enum Week {
    Monday = 1,
    Tuesday,
    Wensday,
    ThirsDay,
    Friday,
    Sarturday,
    Sunday
}

// 可以看的出数字枚举TS会自动推断出Friday的值为5。
console.log(Week.Friday);// 5
// 数字枚举不仅可以通过key取值,还能通过值取key
console.log(Week[5]);// Friday
enum Week {
    Monday = "MyMonday",
    Tuesday = "MyTuesday",
    Wensday = "Wensday",
    ThirsDay = "ThirsDay",
    Friday = "Friday",
    Sarturday = "Sarturday",
    Sunday = "sunday"
}

// 字符串类型的枚举不能推断出值
console.log(Week.ThirsDay); // ThirsDay
// 字符串类型枚举不能通过值取key
console.log(Week["MyMonday"]); // 会报错

11.类型推论|类型别名

类型推论是说TS有能力推断出这个变量的类型是什么

let a = '123' // 推断出a的类型是string
a = 123       // 这个时候就会报错了,因为不能把number赋给string类型

类型别名就是说给一个类型命名,在类型比较复杂的时候会用到

// 假如说我们有一些数据
let a: string | number = 1
let b: string | number = 2
let c: string | number = 3

// 每次都要重复写 string | number 就会很烦

// 用一个TypeData来把string | number装起来,方便使用
type TypeData = string | number
let e: TypeData = 4
let d: TypeData = 5
let g: TypeData = '6'
// extends在type中表示包含
// 下面的代码表示1是否作为number的子类型
type num = 1 extends number ? 1 : 0

12.never

never 类型在 TS中表示永远不会出现的值的类型。它有几个重要的使用场景和应用:

// 函数一运行就抛出错误,这样的就是never
function throwError(message: string): never {
    throw new Error(message);
}

// 无限循环也是never
function infiniteLoop(): never {
    while (true) {
        // 无限循环
    }
}
// 在switch的判断中,用来兜底的
// 这样做的好处:可以尝试一下,如果说添加了一个d,但是忘记改fn中的逻辑,那么就会有报错提示,
type A = 'a' | 'b' | 'c' 
 
function fn(value:A) {
   switch (value) {
       case "a":
           break 
       case "b":
          break 
       case "c":
          break 
       default:
          //是用于场景兜底逻辑
          const error:never = value;
          return error
   }
}

13.symbol类型

symbol类型的值是通过Symbol构造函数创建的。

// 就算参数一致,构造出来的symbol也是不一样的
const sym1 = Symbol('value')
const sym2 = Symbol('value')

// 严格模式下会出现报错提示:此比较似乎是无意的,因为类型“typeof sym1”和“typeof sym2”没有重叠。
// 这证明了sym1必不可能和sym2相等
console.log(sym1 === sym2); 

symbol类型的数据经常用作对象的key

// 就算参数一致,构造出来的symbol也是不一样的
const sym = Symbol('value')
let obj = {
    [sym]: 123
}

console.log(obj[sym]); // 123

对象中symbol类型的key是没有办法通过遍历拿到的:

const sym = Symbol('value')

let obj = {
    [sym]: 123
}

// 控制台并不会输出任何东西
for (const key in obj) {
    console.log(key);
}

那么我们如何才能拿到呢?

// Object.getOwnPropertySymbols()
const symKeys = Object.getOwnPropertySymbols(obj)
console.log(symKeys); // 控制台有输出
// Reflect.ownKeys(),可以拿到所有的key,包括symbol类型的key
const objKeys = Reflect.ownKeys(obj)
console.log(symKeys);

14.泛型

泛型也是一种数据类型,有以下特点:

特点一:定义时不明确使用时必须明确某种具体数据类型的数据类型。

特点二:编译期间进行数据类型检查的数据类型。

// 定义这个接口的时候,并不知道value属性是什么类型,就用一个参数代替
interface Ref<T> {
    value: T
}

// 定义的时候不明确,但是使用的时候就要明确,需要传入一个类型
let ref: Ref<string> = {
    value: "string"
}

再举个例子为什么要这么用呢?

// 假如有这样两个方法
// 观察发现这两个方法几乎一模一样,参数和返回值的类型不一样
// 那有没有什么好的方法只写一个方法就能解决呢?
const fn1 = (a: string, b: string): string[] => {
    return [a, b]
}

const fn2 = (a: number, b: number): number[] => {
    return [a, b]
}

// 那么这个时候泛型的优势就凸显出来了
// 这里其实还用到了反向推断泛型,自动推出了T的类型为number
// 不反推的话也可以这样用 let arr = fn3<number>(1, 2)
const fn3 = <T>(a: T, b: T): T[] => {
    return [a, b]
}
let arr = fn3(1, 2)

更多的例子:

// 类型别名使用泛型
type A<T> = string | number | T

// 接口使用泛型
interface B<T> {
    a: T
}

// 函数使用泛型,除了上面用到的箭头函数,也可以写关键词function定义的函数
function fn<T>(a: T, b: T) {
    return [a, b]
}

15.泛型约束

// 这样写肯定会报错的,因为有无法相加的可能出现,比如undefined + undefined
const fn = <T>(a: T, b: T) => {
    return a + b // 会出现报错提示
}

// 这个泛型也太泛了,需要我们对他进行约束,保证可以相加
// 在泛型中,extends就表示左边的类型是右边类型的子集
const fn2 = <T extends number>(a: T, b: T) => {
    return a + b
}

// 再举个例子
interface Len {
    length: number
}
//这样做的目的就是保证T里面有length这个属性
const fn3 = <T extends Len>(data: T) => {
    return data.length
}

16.keyof

keyof 操作符接收一个对象类型,并返回由该类型的所有键组成的字符串或数字字面量联合类型:

// 使用typeof来得到这个obj的类型,鼠标悬浮可以查看到
type A = {
    name: string;
    age: number;
    id: number;
}

// B类型实际是 'name' | 'age' | 'id'
type B = keyof A

keyof常常与泛型结合,特别是创建通用类型安全的函数时候:

// 写一个获取对象属性值的函数
// K的类型收到约束,必须是 keyof T的子集
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

// 这里推出了T的类型为
// {
//   name: string;
//   age: number;
//   address: string;
//}
// 那么K的类型就为 'name'|'age'|'address'
const person = {
    name: "zs",
    age: 30,
    address: "Wonderland"
};

// 类型安全的属性访问
const name1 = getProperty(person, "name"); // 类型为 string
const age = getProperty(person, "age");   // 类型为 number
 // 错误:参数'"job"'不能赋给类型'"name" | "age" | "address"'
// getProperty(person, "job"); 

17.namespace命名空间

先说一下这个功能用的比较少了,大部分都是用ES 模块了,但是还是可以了解一下的。

比如说我们在做一些跨端项目,需要比较清晰的分类方法:

// 假如我们做一个项目,里面又有H5的方法,又有安卓的方法,又有IOS的方法
namespace H5 {

    // 需要注意,必须使用export导出这个方法才能在namespace外面使用
    export const h5Fn = () => {
        console.log('H5方法');
    }
}


namespace Android {
    export const androidFn = () => {
        console.log('Android方法');
    }
}

namespace IOS {
    export const iosFn = () => {
        console.log('IOS方法');
    }
}

namespace Test1 {
    // 命名空间也是可以嵌套的,同样也需要导出
    export namespace Test2 {
        export const testFn = () => {
            console.log('Test2方法');
        }
    }
}

H5.h5Fn()
Android.androidFn()
// 调用嵌套命名空间的方法
Test1.Test2.testFn()

// 还有一些规则,命名空间也是可以想interface一样合并的,可以自行测试

18.模块解析

AMDCMD奇奇怪怪的自行了解吧,我们现在常用的模块规范CommonJS还有ESM(ES6模块化)。

// CommonJS
// 导出

//-------------------
// math.js
module.exports = {
  add: function(a, b) { return a + b; },
  multiply: function(a, b) { return a * b; }
};
// 或者单独导出
exports.subtract = function(a, b) { return a - b; };
//-------------------

// 导入
const math = require('./math');
math.add(2, 3); // 5
// ESM
// 导出

//------------------------
// math.js
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
// 默认导出
export default function divide(a, b) { return a / b; }
//------------------------

// 导入
import { add, multiply } from './math.js';
import divide from './math.js';
// 导入所有
import * as math from './math.js';

他们俩之间的不同点:

  1. 语法

    // CommonJS使用 require()引入 module.exports导出
    // ESM使用import引入, export导出
  2. 加载时机

    // CommonJS是运行的时候同步加载,整个模块对象被加载进内存
    // ESM是编译时候加载,确定依赖关系,运行的时候异步加载
  3. 导入类型

    // CommonJS是值的拷贝,如果导出的值在原模块中改变,导入的值不会变
    // ESM是对变量本身的引用
  4. 环境

    // CommonJS node.js中
    // ESM 浏览器环境

19.declare声明文件

当我们在项目中安装了axios时候引入axios并不会出现什么报错:

// 不会报错
// 按住Ctrl点击axios能进入axios的声明文件
import axios from "axios";

但是当我们安装了一些比较老的库,没有声明文件,就会出现报错。例如express

// express下飘红报错了
import express from 'express'

遇到这种情况,可以根据提示中执行npm i --save-dev @types/express来解决,或者手写声明文件,下面来尝试手写声明文件。

// 首先创建一个typings用来存放声明文件
// 然后在typings下新建一个express.d.ts
// index.ts
import express from 'express'
 
 
const app = express()
 
const router = express.Router()
 
app.use('/api', router)
 
router.get('/list', (req, res) => {
    res.json({
        code: 200
    })
})
 
app.listen(9001,()=>{
    console.log(9001)
})
// express.d.ts
// 基本就是哪里缺什么就补什么
declare module 'express' {
    interface Router {
        get(path: string, cb: (req: any, res: any) => void): void
    }
    interface App {
 
        use(path: string, router: any): void
        listen(port: number, cb?: () => void): void
    }
    interface Express {
        (): App
        Router(): Router
 
    }
    const express: Express
    export default express
}

20.mixin混入

对象的混入

const obj1 = {
    name: 'zs'
}

const obj2 = {
    age: 18
}

// 1.使用扩展运算符
const obj3 = {
    ...obj1, ...obj2
}
// 2.使用 Object.assign
const obj4 = Object.assign({}, obj1, obj2)

//注意了这两种方式实际都是浅拷贝(只有第一层的属性会脱离引用)

// 题外话:如何深拷贝呢?使用 structuredClone
const deepcloneObj = structuredClone(obj1)

类的混入

// 定义 Mixin 的构造函数类型
type Constructor<T = {}> = new (...args: any[]) => T;

// 定义可飞行 Mixin
function CanFly<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    fly() {
      console.log("Flying!");
    }
  };
}

// 定义可游泳 Mixin
function CanSwim<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    swim() {
      console.log("Swimming!");
    }
  };
}

// 组合 Mixin
class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

const FlyingSwimmingAnimal = CanSwim(CanFly(Animal));

// 使用
const superDuck = new FlyingSwimmingAnimal("Duck");
superDuck.fly(); // "Flying!"
superDuck.swim(); // "Swimming!"

21.装饰器

首先要把tsconfig.json中的这两项配置打开(不然可泵会有报错):

然后安装tsc-node,后面编译运行就用tsc-node 文件名

npm install -g typescript@latest

什么是装饰器:看例子

下面这个例子说明了装饰器的好处就是节省了代码,然后也不用去破坏A的结构。

// 写一个类装饰器函数,会把自动把类的构造函数传入当做第一个参数
const Watcher: ClassDecorator = (target) => {
    target.prototype.getParams = <T>(params: T): T => {
        return params
    }
}

// 用的时候使用@+函数名就可以了
@Watcher
class A {
    constructor() {

    }
}

const a = new A()
console.log((a as any).getParams('abc')); // abc

装饰器工厂:

说白了就是使用@Watcher的时候我想传入一个参数咋办:


// 我们就给他再包一层用来接受参数,里面返回一个普通的装饰器函数
const Watcher = (value: string) => {
    const fn: ClassDecorator = (target) => {
        target.prototype.getParams = <T>(params: T): T => {
            return params
        }
        target.prototype.value = value
    }
    return fn
}

@Watcher('123')
class A {
    constructor() {

    }
}

const a = new A() as any
console.log(a.getParams('abc')); // abc
console.log(a.value); // 123

进一步来看看方法装饰器:

const meet = (name: string) => {
    const fn: MethodDecorator = (value, key, descriptor) => {
        // 可以打印这三个参数看看
        console.log(value, key, descriptor);
        // value:{}  就是原型对象
        // key:getName 对应的方法名
        // descriptor:{
        //                 value: [Function: getName],
        //                 writable: true,
        //                 enumerable: false,
        //                 configurable: true
        //             }

        console.log('name', name);
    }
    return fn
}

class A {
    constructor() {

    }
    @meet('zs')
    getName(): string {
        return 'ls'
    }
}

// 编译过后控制台输出了zs

属性装饰器:

const met: PropertyDecorator = (...args) => {
    console.log(args);
}

class A {
    @met
    name: string = 'zs'
    constructor() {
    }

}

// 控制台输输出 [ {}, 'name', undefined ]

参数装饰器:

const met: ParameterDecorator = (...args) => {
    console.log(args);
}

class A {
    constructor() {

    }
    setParasm(@met name: string = '213') {

    }
}

// 控制台输出 [ {}, 'setParasm', 0 ]
// 前面两个就不说了,0就跟表示位置,第一个0,第二个就是1

22.发布订阅

发布订阅就是一种设计模式,是一种思想没有固定的代码。

很多框架都在使用比如Vueelectron,还有一些组件通信插件eventBusaddEventListener等等。

// 这个其实就是一个发布订阅,这是一个触发器
document.addEventListener('click', function () {
    console.log('点击了');
});


// 如何自己实现一个事件呢
document.addEventListener('abc', function () {
    // 控制台输出了 点击了abc
    console.log('点击了abc');
});

// 创建了一个事件,相当于一个订阅中心
const e = new Event('abc')
// 使用dispatchEvent派发这个事件
document.dispatchEvent(e)

手写来实现发布订阅:


interface I {
    events: Map<string, Function[]> // 事件的数组
    once: (event: string, callback: Function) => void // 只触发一次的功能
    on: (event: string, callback: Function) => void // 订阅
    emit: (event: string, ...args: any[]) => void // 派发
    off: (event: string, callback: Function) => void // 删除
}


class Eimitter implements I {
    events: Map<string, Function[]>
    constructor() {
        this.events = new Map()
    }

    // 只触发一次的功能
    once(event: string, callback: Function) {
        const fn = (...args: any[]) => {
            callback(...args)
            // 立马回收
            this.off(event, fn)
        }
        this.on(event, fn)
    }

    on(event: string, callback: Function) {
        if (this.events.has(event)) {
            // 说明存过了
            const callbackList = this.events.get(event)
            // 第二次存就把事件push进去
            callbackList && callbackList.push(callback)
        } else {
            // 说明第一次存
            this.events.set(event, [callback])
        }
    }

    emit(event: string, ...args: any[]) {
        const callbackList = this.events.get(event)
        if (callbackList) {
            callbackList.forEach((fn) => {
                fn(...args)
            })
        }
    }

    off(event: string, callback: Function) {
        const callbackList = this.events.get(event)
        if (callbackList) {
            // 说明有这个事件
            const index = callbackList.indexOf(callback)
            if (index !== -1) {
                // 说明有这个事件
                callbackList.splice(index, 1)

            }
        }
    }
}

const bus = new Eimitter()

// 定义callback
const fn1 = (b: boolean, n: number) => {
    console.log(1, b, n);
}

const fn2 = (b: boolean, n: number) => {
    console.log(2, b, n);
}

bus.on('message', fn1)

bus.on('message', (b: boolean, n: number) => {
    console.log(2);
})

// 取消订阅fn1
bus.off("message", fn1)

bus.emit('message', false, 1)

bus.once('message2', fn2)

// 控制台可以看到就算触发多次也只会触发一次
bus.emit('message2', true, 3)
bus.emit('message2', true, 3)
bus.emit('message2', true, 3)
bus.emit('message2', true, 3)
bus.emit('message2', true, 3)

23.类型守卫

用一下这些方法,可以看出来,能够把传入的any类型的数据做一个过滤,过滤到想要的类型,这个也就是类型收缩

// typeof
// typeof可以用来判断基本数据类型,但是不能用来判断引用数据类型
const isString = (val: any) => typeof (val) === 'string'

console.log(isString('123')); // true
console.log(isString(123)); // false

// instanceof
// instanceof可以用来判断引用数据类型
const isObject = (val: any) => val instanceof Object
const isAarry = (val: any) => val instanceof Array

console.log(isObject({ a: '123' })); // true
console.log(isAarry([11, 2, 3])); // true

// Array.isAarry
// Array.isArray()可以用来判断数组
const isAarry2 = (val: any) => Array.isArray(val)
console.log(isAarry2([12, 2, 3])); // true

类型谓词:

首先来看一个小题目。

// 实现一个函数,该函数可以传入任何类型
// 但是如果是object 就检查里面的属性,如果里面的属性是number就取两位
// 如果是string就去除左右空格
// 如果是函数就执行

const isString = (data: any) => typeof (data) === 'string'
const isNumber = (data: any) => typeof (data) === 'number'
const isFunction = (data: any) => typeof (data) === 'function'

// 一般写这个就是太长一串了Object.prototype.toString.call(data) === '[object Object]'
// 语法糖简写 ({}).toString.call(data) === '[object Object]'
const isObject = (data: any) => ({}).toString.call(data) === '[object Object]'

const fn = (data: any) => {

    if (isObject(data)) {
        // 不能使用 for in 遍历,因为for in 遍历的是对象的原型链上的属性
        // Object.keys(data) 返回一个数组,数组的元素是data对象的属性名
        Object.keys(data).forEach(key => {
            let val = data[key]
            // data[key] 取出data对象的属性值
            if (isNumber(val)) {
                data[key] = val.toFixed(2)
            } else if (isString(val)) {
                data[key] = val.trim()
            } else if (isFunction(val)) {
                data[key]()
            }
        })
    }
    return data
}


const obj = {
    a: 1.123,
    b: ' 123 ',
    c: () => {
        console.log('fn')
    }
}
console.log(fn(obj));

题目不难,但是在写的过程中发现一个问题:

为什么已经使用了自己定义的方法收窄了类型,但是写的时候没有代码提示?而且检查发现val类型是any

这个时候我们就要使用到类型谓词实现自定义守卫:

// 类型谓词能让TS更好的推断变量的类型
// :data is string 如果返回的是true,那么这个data一定就是string
const isString = (data: any): data is string => typeof (data) === 'string'
const isNumber = (data: any): data is number => typeof (data) === 'number'
const isFunction = (data: any): data is Function => typeof (data) === 'function'

加上类型谓词就发现有代码提示了,而且TS能推断出类型了:

23.协变和逆变

协变:

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

interface B {
    name: string;
    age: number;
    sex: string;
}

// 可以发现B的属性完全覆盖了A的属性
let a: A = { name: 'zs', age: 1 };
let b: B = { name: 'ls', age: 18, sex: 'male' };

// 发现b可以赋值给a,但是a不能赋值给b
// 再通俗一点就是a要的属性b都有,但是b要的属性a不一定有
a = b
b = a // 报错

逆变:

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

interface B {
    name: string;
    age: number;
    sex: string;
}

let fn1 = (a: A) => {
    console.log(a);
}

let fn2 = (b: B) => {
    console.log(b);
}

// 如果是一个函数的话,就刚好和协变反过来
// 这个就叫做逆变
fn2 = fn1
fn1 = fn2 // 报错

24.泛型工具

TS一共有内置的许多工具,其实翻译一下就很好理解了。

都以对象类型为例子:

// 定义了一个接口Person
interface Person {
    name: string;
    age: number;
    sex: string;
    height: number;
    weight: number;
}
  1. Partial

    部分的

    把一个对象类型所有的属性变成可选。

    // Partial<T>的作用是将类型T的所有属性变为可选属性
    type PersonPartial = Partial<Person>;
    // Partial<Person>的结果是:
    // type PersonPartial = {
    //     name?: string | undefined;
    //     age?: number | undefined;
    //     sex?: string | undefined;
    //     height?: number | undefined;
    //     weight?: number | undefined;
    // }
  2. Required

    必须的

    把一个对象类型所有的属性变成必须。

    // Required<T>的作用是将类型T的所有属性变为必选属性
    type RquiredPerson = Required<PersonPartial>;
    // Required<Person>的结果是:(可以看出,使用之前PersonPartial都是可选属性的,这下又变回来了)
    // type RquiredPerson = {
    //     name: string;
    //     age: number;
    //     sex: string;
    //     height: number;
    //     weight: number;
    // }
  3. Pick

    选择

    把一个对象类型部分属性选出来新组成一个类型。

    // Pick<T, K>的作用是将类型T的属性K提取出来
    type PickPerson = Pick<Person, 'name' | 'age'>;
    // Pick<Person, 'name' | 'age'>的结果是:
    // type PickPerson = {
    //     name: string;
    //     age: number;
    // }
  4. Exclude

    排除

    把一个联合类型中排除掉一些类型。

    // Exclude<T, U>的作用是将类型T中属于类型U的属性排除掉
    type ExcludePerson = Exclude<keyof Person, 'name' | 'age'>;
    // type ExcludePerson = "sex" | "height" | "weight"
    // Exclude只能用于联合类型,所以keyof Person是一个联合类型,'name' | 'age'也是一个联合类型
  5. Omit

    省略

    把一个对象类型部分属性排除掉。

    // Omit<T, K>的作用是将类型T中的属性K排除掉
    type OmitPerson = Omit<Person, 'name' | 'age'>;
    // type OmitPerson = {
    //     sex: string;
    //     height: number;
    //     weight: number;
    // }
  6. Readonly

    只读

    把一个对象类型所有的属性变成只读。

    // Readonly<T>的作用是将类型T的所有属性变为只读属性
    type ReadonlyPerson = Readonly<Person>;
    // type ReadonlyPerson = {
    //     readonly name: string;
    //     readonly age: number;
    //     readonly sex: string;
    //     readonly height: number;
    //     readonly weight: number;
    // }
  7. Record

    记录

    创造一个对象类型。

    // Record<K, T>的作用是构造一个类型,这个类型的属性名是K中的属性,属性值是T类型
    type RecordPerson = Record<'name' | 'age', string>;
    // type RecordPerson = {
    //     name: string;
    //     age: string;
    // }
  8. ReturnType

    获取这个函数类型的返回值类型

    // ReturnType<T>的作用是获取函数类型T的返回值类型
    const fn = () => {
        return 1
    }
    // 这里的typeof用来获取函数的类型
    type ReturenTypePerson = ReturnType<typeof fn>;
    // ReturnType<() => Person>的结果是:
    // type ReturenTypePerson = number

25.infer

infer 是 TypeScript 中的一个关键字,用于在 条件类型 中推断类型。它的作用是从一个复杂类型中提取出某个部分的类型。

心语语法T extends infer U ? U : never

// 推断出数组中的元素类型
type ElementType<T> = T extends (infer U)[] ? U : never;

// 示例
type Numbers = number[];
type NumberType = ElementType<Numbers>; // number


// 提取出出函数返回值(实现ReturnType)
type ReturnType1<T> = T extends (...args: any[]) => infer R ? R : never;

// 示例
function foo() {
    return 42;
}

type FooReturnType = ReturnType1<typeof foo>; // number
                                 
// 提取函数参数类型                      
type Parameters<T> = T extends (...args: infer P) => any ? P : never;

// 示例
function bar(a: string, b: number) {
  console.log(a, b);
}

type BarParams = Parameters<typeof bar>; // [string, number]
kucha
“ 大冬天的非常冷,打工人很辛苦,需要一杯奶茶暖暖肚子。 ”
 喜欢文章
头像