日常使用 TypeScript 的实践总结

日常使用 TypeScript 的实践总结_第1张图片

Typed JavaScript at Any Scale,Typescript 是添加了类型系统的 JavaScript,适用于任何规模的项目。

keyof T索引查询

结果为该类型上所有公有属性 key 的联合。

interface Eg1 {
  name: string,
  readonly age: number,
}
/** T1的类型实则是name | age */
type T1 = keyof Eg1

class Eg2 {
  private name: string;
  public readonly age: number;
  protected home: string;
}
/** T2实则被约束为 age, 而 name 和 home 不是公有属性,所以不能被keyof获取到 */
type T2 = keyof Eg2;

T[K] 索引访问

interface Person {
  name: string,
  readonly age: number,
}
/** string */
type V1 = Person['name']
/** string | number */
type V2 = Person['name' | 'age']
/** Error:Property 'address' does not exist on type 'Person'.  */
type V3 = Person['name' | 'address']
/** string | number */
type V4 = Person[keyof Person]

T[keyof T]的方式,可以获取到 T所有 key的类型组成的联合类型; T[keyof K]的方式,获取到的是 T中的 key且同时存在于K时的类型组成的联合类型; 注意:如果[]中的 key有不存在 T中的,则ts也不知道该key最终是什么类型,所以会报错;

交叉类型 & 

交叉类型是取多个类型的并集,但是如果相同 key但是类型不同,则该 key为 never

日常使用 TypeScript 的实践总结_第2张图片

extends

        1. extends 用于接口,表示继承,接口支持多重继承,语法为逗号隔开:

interface T1 {
  name: string,
}

interface T2 {
  sex: number,
}

/**
 * @example
 * T3 = {name: string, sex: number, age: number}
 */
interface T3 extends T1, T2 {
  age: number,
}

        2. extends 表示条件类型,可用于条件判断,extends前面的参数为联合类型时则会分解(依次遍历所有的子类型进行条件判断)联合类型进行判断。然后将最终的结果组成新的联合类型。如果不想被分解,可以通过简单的元组类型包裹。

/**
 * @example
 * type A3 = 1 | 2
 */
type P = T extends 'x' ? 1 : 2;
type A3 = P<'x' | 'y'>

/**
 * @example
 * type A4 = 2;
 */
type P = [T] extends ['x'] ? 1 : 2;
type A4 = P<'x' | 'y'>

类型兼容性 

集合论中,如果一个集合 A 的所有元素在集合B中都存在,则A是B的子集;

类型系统中,如果一个类型的属性更具体,则该类型是子类型。(对于 inteface 中,属性更少则说明该类型约束的更宽泛,是父类型,对于联合类型,包括种类越多的类型是父类型)

  1. 可赋值性:只有更具体的子类型可以赋值给更宽泛的父类型。
  2. 协变:具有父子关系的多个类型,在通过某种构造关系构造成的新的类型,如果还具有父子关系则是协变的。在 Animal和 Dog在变成数组后,Array依旧可以赋值给Array,因此对于 type MakeArray = Array来说就是协变的:
interface Animal {
  name: string;
}

interface Dog extends Animal {
  break(): void;
}

const animal: Animal;
const dog: Dog;
const animals: Array
const dogs: Array

/** 可以赋值 */
animal = dog;
/** 可以赋值 */
animal = dogs

        3. 逆变:具有父子关系的多个类型,在通过某种构造关系构造成的新的类型,如果关系逆转(子变父,父变子)就是逆变的。Animal和 Dog在进行 type Fn = (arg: T) => void构造器构造后,父子关系逆转:

日常使用 TypeScript 的实践总结_第3张图片 animalFn 在调用时约束的参数,缺少 dogFn 的参数所需的 break,此时会导致错误。

        4. 双向协变:Ts在函数参数的比较中实际上默认采取的是只有当源函数参数能够赋值给目标函数或者反过来时才能赋值成功的双向协变的策略。可以通过tsconfig.js中修改 strictFunctionType属性来严格控制协变和逆变。infer关键词的功能主要是用于extends的条件类型中让 TS 自己推到类型,并将推导结果存储在其参数绑定的类型上,比如 infer P 就是将结果存在类型 P上,infer关键词只能在 extends条件类型上使用。

  • infer推导的名称相同并且都处于逆变的位置,则推导的结果将会是交叉类型

日常使用 TypeScript 的实践总结_第4张图片

  • infer推导的名称相同并且都处于协变的位置,则推导的结果将会是联合类型

日常使用 TypeScript 的实践总结_第5张图片

进阶——infer实现一个推导数组所有元素的类型: 

/**
 * 约束参数T为数组类型,
 * 判断T是否为数组,如果是数组类型则推导数组元素的类型
 */
type FalttenArray> = T extends Array ? P : never;

Readonly

const进行常量声明且不可修改,如果进行修改的话会直接 Cannot assign to 'a' because it is a constant.进行异常抛错。但是如果值是一个引用类型的话,依旧可以对其内部的属性进行修改。那么从只读的概念上来说,显然不具备当前的能力。可以使用以下四种方式对属性或者是变量进行声明达到只读的效果。

/** 1. Readonly */
interface X {
  x: number;
}
let rx: Readonly = { x: 1 };
rx.x = 12; // error

/** 2. readonly modifier for properties */
interface Rx {
  readonly x: number;
}
let rx: Rx = { x: 1 };
rx.x = 12; // error

/** 3. ReadonlyArray */
let a: ReadonlyArray = [1, 2, 3];
let b: readonly number[] = [1, 2, 3];
a.push(102); // error
b[0] = 101; // error

/** 4. as const */
let a = [1, 2, 3] as const;
a.push(102); // error
a[0] = 101; // error

Readonly 实现原理:

/**
 * 主要实现是通过映射遍历所有key,
 * 然后给每个key增加一个readonly修饰符
 */
type Readonly = {
  readonly [P in keyof T]: T[P]
}

条件类型(Conditional Type)

通过extends的方式继承父类然后通过 ? : 表达式来进行一个类型三目运算符的操作进行一个类型的条件判断。

interface Animal {
  live(): void;
}
interface Dog extends Animal {
  woof(): void;
}

type Example1 = Dog extends Animal ? number : string;

type Example2 = RegExp extends Animal ? number : string;

namespace

命名空间(namespace)常用于组织一份类型区域防止类型之间的重命名冲突,需要配置 declare 输出到外部环境才能够使用,使用 declare namespace在工程项目中可以不需要引入任何类型而直接可以访问,非常便捷。

declare namespace myLib {
  function makeGreeting(s: string): string;
  let numberOfGreetings: number;
}

declare

declare是用于声明存在的

  • declare var/let/const用来声明全局的变量。
  • declare function 用来声明全局方法(函数)
  • declare class 用来声明全局类
  • declare namespace 用来声明命名空间
  • declare module 用来声明模块
  • ...

需要注意的是,declaredeclare global它们功能是一样的。在d.ts中,使用declare即可,默认是全局的,它declare global作用是相等的。而在模块文件中定义declare,如果想要用作全局就可以使用declare global。

/** types/foo/index.d.ts */
declare global {
    interface String {
        prependHello(): string;
    }
}
/** 注意即使此声明文件不需要导出任何东西,仍然需要导出一个空对象,用来告诉编译器这是一个模块的声明文件,而不是一个全局变量的声明文件。 */
export {};

/** src/index.ts 使用 */
'bar'.prependHello();

若果需要扩展原有模块的话,需要在类型声明文件中先引用原有模块,再使用 declare module 扩展原有模块:

/** types/moment-plugin/index.d.ts */
import * as moment from 'moment';

declare module 'moment' {
    export function foo(): moment.CalendarKey;
}

/** src/index.ts */
import * as moment from 'moment';
import 'moment-plugin';

moment.foo();

declare module 也可用于在一个文件中一次性声明多个模块的类型:

/** types/foo-bar.d.ts */

declare module 'foo' {
    export interface Foo {
        foo: string;
    }
}

declare module 'bar' {
    export function bar(): string;
}

/** src/index.ts */
import { Foo } from 'foo';
import * as bar from 'bar';

let f: Foo;
bar.bar();

模板字符串类型

模板字符串能够对文本进行一定程度上的约束。

/** global.d.ts */
declare type HTTP = `http://${string}`
declare type HTTPS = `https://${string}`

/** @/config/index.d.ts */
type baseApi = HTTP | HTTPS;

约定当前值中必须包含http://或者是https://才算校验成功。

函数重载

函数重载大多数用于多态函数,它能够定义不同的参数类型。需要有多个重载签名和一个实现签名

  • 重载签名:就是对参数形式的不同书写,可以定义多种模式。
  • 实现签名:对函数内部方法的具体实现。
function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string | void {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''));
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('');
    }
}

getter/setter

get/set存取器保证了类中变量的私有化,在外部时时不能直接对其更改的。在class中声明一个带_下标的变量,那么就可以通过getset对其进行设置值。

日常使用 TypeScript 的实践总结_第6张图片

如果访问了私有化类型的变量会在编译时就发出告警。 

枚举

枚举(enum)的使用场景在于可以定义部分行为和状态,对其某个任务的行为定义在枚举当中,这样做可以进行一些状态复用,避免在页面写太多魔数而不利于维护。

日常使用 TypeScript 的实践总结_第7张图片

泛型

泛型是TypeScript在类型推导很难进行推导。相比于使用 any 类型,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类型。

type Generics = {
    name: string
    age: number
    sex: T
}

function setSex (sex: T) {
}

class Person {
    private sex: T;
    constructor(readonly type: T) { 
        this.sex = type; 
    }
}

Record

Record能够快速创建对象类型。它的使用方式是Record,能够快速的为object创建统一的keyvalue类型。

Record 的实现原理:

/**
 * 核心实现就是遍历 K,将值设置为 T
 */
type Record = {
  [P in K]: T
}

值得注意的是 keyof any得到的是 string | number | symbol,原因在类型 key的类型只能为 string | number | symbol。

Pick & Omit 

Pick:主要作用是从一组属性中拿出某个属性,并将其返回。Pick的使用方法是Pick,如(P)类型中拥有name,age,desc三个属性,那么K为 name则最终将取到只有name的属性,其他的将会被排出。Pick 实现原理:

type Pick = {
    [P in K]: T[P];
};

// 或者
type Pick = {
    [P in keyof T]: T[P];
}

Omit:主要作用是从一组属性中排除某个属性,并将排除属性后的结果返回。Omit的使用方法是Omit,与Pick的结果是相反的,如果说Pick是取出,那么Omit则是过滤的效果。Omit 的实现原理:

/**
 * 利用Pick实现Omit
 */
type Omit = Pick>;

// 或者

/**
 * 利用映射类型Omit
 */
type Omit2 = {
  [P in Exclude]: T[P]
}

Exclude & Extract

Exclude: 从一个联合类型中排除掉属于另一个联合类型的子集。Exclude使用形式是Exclude,如果联合类型 T中的在 S中不存在那么就会返回,相当于取差集。

日常使用 TypeScript 的实践总结_第8张图片

Exclude的实现原理:

/**
 * 遍历 T 中的所有子类型,如果该子类型约束于 U(存在于U、兼容于U),则返回 never 类型,否则返回该子类型
 * never 表示一个不存在的类型,never 与其他类型的联合后,是没有 never 的。
 */
type Exclude = T extends U ? never : T;

Extract跟Exclude相反,从从一个联合类型中取出属于另一个联合类型的子集。Extract就是取交集。会返回两个联合类型中相同的部分。

日常使用 TypeScript 的实践总结_第9张图片

Extract 的实现原理:

type Extract = T extends U ? T : never;

Partial

Partial是一个将类型转为可选类型的工具,对于不明确的类型来说,需要将所有的属性转化为可选的?.形式,转换成为可选的属性类型。

/**
 * 核心实现就是通过映射类型遍历T上所有的属性,
 * 然后将每个属性设置为可选属性
 */
type Partial = {
  [P in keyof T]?: T[P];
}

日常使用 TypeScript 的实践总结_第10张图片

进阶——将指定的key变成可选类型:

/**
 * 将指定的key变成可选类型:
 *     主要通过K extends keyof T约束K必须为keyof T的子类型
 *     keyof T得到的是T的所有key组成的联合类型
 */
type PartialOptional = {
  [P in K]?: T[P];
}

 PartialReadonly和 Pick都属于同态的,即他们的实现需要 keyof T 遍历输入类型 T来拷贝属性,因此属性修饰符(例如readonly、?:)都会被拷贝。而 Record是非同态的,不需要拷贝属性,因此不会拷贝属性修饰符。

Parameters 和 ReturnType

Parameters 获取函数的参数类型,将每个参数类型放在一个元组中,实现原理:

/**
 * @desc 具体实现
 */
type Parameters any> = T extends (...args: infer P) => any ? P : never;

/**
 * 或者
 */
type Parameters = T extends (...args: infer P) => any ? P : never;

ReturnType 获取函数的返回值类型,实现原理:

/**
 * @desc ReturnType的实现其实和Parameters的基本一样
 * 无非是使用infer R的位置不一样。
 */
type ReturnType any> = T extends (...args: any) => infer R ? R : any;

ConstructorParameters

ConstructorParameters 获取类的构造函数的参数类型,存在一个元组中,实现原理:

/**
 * 核心实现还是利用infer进行推导构造函数的参数类型
 */
type ConstructorParameters any> = T extends abstract new (...args: infer P) => any ? P : never;

首先约束参数 T为拥有构造函数的类(使用 abstract 关键字将类型定义为抽象类,则既可以赋值为抽象类,也可以赋值为普通类;反之如果只是定义为普通类,则只能赋值为普通类),实现时,判断T是满足约束的类时,利用infer P自动推导构造函数的参数类型,并最终返回该类型。

/**
 * 定义一个普通类
 */
class MyClass {}
/**
 * 定义一个抽象类
 */
abstract class MyAbstractClass {}

/** 可以赋值 */
const c1: typeof MyClass = MyClass */
/** 报错,无法将抽象构造函数类型分配给非抽象构造函数类型
const c2: typeof MyClass = MyAbstractClass

/** 可以赋值 */
const c3: typeof MyAbstractClass = MyClass
/** 可以赋值 */
const c4: typeof MyAbstractClass = MyAbstractClass

需要注意的是:

  • 当把类直接作为类型时,该类型约束的是——该类型必须是类的实例;即该类型获取的是该类上的实例属性和实例方法(也叫原型方法);
  • 当把 typeof 类作为类型时,约束的是——满足该类的类型;即该类型获取的是该类上的静态属性和方法。

进阶——获取构造函数返回值的类型:

type InstanceType any> = T extends abstract new (...args: any) => infer R ? R : any;

Ts compiler 内部实现的类型——Uppercase & Lowercase & Capitalize & Uncapitalize

/**
 * Uppercase
 * @desc 构造一个将字符串转大写的类型
 */
type Eg1 = Uppercase<'abcd'>;

/**
 * Lowercase
 * @desc 构造一个将字符串转小大写的类型
 */
type Eg2 = Lowercase<'ABCD'>;

/**
 * Capitalize
 * @desc 构造一个将字符串首字符转大写的类型
 */
type Eg3 = Capitalize<'Abcd'>;

/**
 * Uncapitalize
 * @desc 构造一个将字符串首字符转小写的类型
 */
type Eg4 = Uncapitalize<'aBCD'>;

 TS 类型体操 —— 实现EventEmitter

实现一个 EventEmitter 类,该类中存在两个方法 on / emit。on(functionName, function) 方法可以订阅一个 functionName 的函数。emit(functionName, ...args) 方法可以调用 on 方法中注册的函数,获得对应的函数类型。

class EventEmitter> {
  private fnMap = new Map void>();
  on(key: K, fn: (...args: T[K]) => void): void {
    this.fnMap.set(key, fn);
  }

  emit(key: K): (...args: T[K]) => void {
    const fn = this.fnMap.get(key);
    return (...args: T[K]) => {
      fn && fn(...args);
    };
  }
}

const eventEmitter = new EventEmitter<{
    getName: [string],
    getAge: [number],
}>();

eventEmitter.on('getName', (name: string) => {
    console.log(name)
});

eventEmitter.emit('getName')('edemao');

eventEmitter.on('getAge', (age: number) => {
    console.log(age)
});

eventEmitter.emit('getAge')(26);

你可能感兴趣的:(前端,typescript,前端)